tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from datetime import datetime, timedelta
  41from dateutil.tz import tzlocal, tzutc
  42from time import sleep
  43
  44import re
  45import json
  46import requests
  47import traceback as tb
  48from typing import Union
  49
  50from multiprocessing import cpu_count
  51from multiprocessing.pool import ThreadPool
  52import pandas as pd
  53
  54from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  55
  56from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  57from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  58
  59import UniLogger as uLog  # Logger for TKSBrokerAPI
  60
  61
  62# --- Common technical parameters:
  63
  64PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  65uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  66uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  67uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  68
  69__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  70
  71CPU_COUNT = cpu_count()  # host's real CPU count
  72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  73
  74# --- Main constants:
  75
  76NANO = 0.000000001  # SI-constant nano = 10^-9
  77
  78
  79def NanoToFloat(units: str, nano: int) -> float:
  80    """
  81    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
  82
  83    `NanoToFloat(units="2", nano=500000000) -> 2.5`
  84
  85    `NanoToFloat(units="0", nano=50000000) -> 0.05`
  86
  87    :param units: integer string or integer parameter that represents the integer part of number
  88    :param nano: integer string or integer parameter that represents the fractional part of number
  89    :return: float view of number
  90    """
  91    return int(units) + int(nano) * NANO
  92
  93
  94def FloatToNano(number: float) -> dict:
  95    """
  96    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
  97
  98    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
  99
 100    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
 101
 102    :param number: float number
 103    :return: nano-type view of number: `{"units": "string", "nano": integer}`
 104    """
 105    splitByPoint = str(number).split(".")
 106    frac = 0
 107
 108    if len(splitByPoint) > 1:
 109        if len(splitByPoint[1]) <= 9:
 110            frac = int("{}{}".format(
 111                int(splitByPoint[1]),
 112                "0" * (9 - len(splitByPoint[1])),
 113            ))
 114
 115    if (number < 0) and (frac > 0):
 116        frac = -frac
 117
 118    return {"units": str(int(number)), "nano": frac}
 119
 120
 121def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 122    """
 123    Create tuple of date and time strings with timezone parsed from user-friendly date.
 124
 125    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 126
 127    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 128    An error exception will occur if input date has incorrect format.
 129
 130    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 131    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 132    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 133    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
 134
 135    Also, you can use keywords for start if `end=None`:
 136    `today` (from 00:00:00 to the end of current day),
 137    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 138    `week` (-7 day from 00:00:00 to the end of current day),
 139    `month` (-30 day from 00:00:00 to the end of current day),
 140    `year` (-365 day from 00:00:00 to the end of current day),
 141
 142    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 143             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
 144             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 145    """
 146    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 147    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 148    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 149
 150    # time between start and the end of the current day:
 151    if start is None or start.lower() == "today":
 152        pass
 153
 154    # from start of the last day to the end of the last day:
 155    elif start.lower() == "yesterday":
 156        s -= timedelta(days=1)
 157        e -= timedelta(days=1)
 158
 159    # week (-7 day from 00:00:00 to the end of the current day):
 160    elif start.lower() == "week":
 161        s -= timedelta(days=6)  # +1 current day already taken into account
 162
 163    # month (-30 day from 00:00:00 to the end of current day):
 164    elif start.lower() == "month":
 165        s -= timedelta(days=29)  # +1 current day already taken into account
 166
 167    # year (-365 day from 00:00:00 to the end of current day):
 168    elif start.lower() == "year":
 169        s -= timedelta(days=364)  # +1 current day already taken into account
 170
 171    # -N days ago to the end of current day:
 172    elif start.startswith('-') and start[1:].isdigit():
 173        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 174
 175    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 176    else:
 177        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 178        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 179
 180    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 181    s = s.strftime(TKS_DATE_TIME_FORMAT)
 182    e = e.strftime(TKS_DATE_TIME_FORMAT)
 183
 184    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 185
 186    return s, e
 187
 188
 189class TinkoffBrokerServer:
 190    """
 191    This class implements methods to work with Tinkoff broker server.
 192
 193    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 194
 195    About `token`: https://tinkoff.github.io/investAPI/token/
 196    """
 197    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 198        """
 199        Main class init.
 200
 201        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 202        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 203                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 204        :param useCache: use default cache file with raw data to use instead of `iList`.
 205                         True by default. Cache is auto-update if new day has come.
 206                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 207        :param defaultCache: path to default cache file. `dump.json` by default.
 208        """
 209        if token is None or not token:
 210            try:
 211                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 212                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 213
 214            except KeyError:
 215                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 216                raise Exception("Token required")
 217
 218        else:
 219            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 220            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 221
 222        if accountId is None or not accountId:
 223            try:
 224                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 225                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 226
 227            except KeyError:
 228                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 229
 230        else:
 231            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 232            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 233
 234        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 235        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 236
 237        Latest version: https://pypi.org/project/tksbrokerapi/
 238        """
 239
 240        self.aliases = TKS_TICKER_ALIASES
 241        """Some aliases instead official tickers.
 242
 243        See also: `TKSEnums.TKS_TICKER_ALIASES`
 244        """
 245
 246        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 247
 248        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 249
 250        self.ticker = ""
 251        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 252
 253        See also: `SearchByTicker()`, `SearchInstruments()`.
 254        """
 255
 256        self.figi = ""
 257        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 258
 259        See also: `SearchByFIGI()`, `SearchInstruments()`.
 260        """
 261
 262        self.depth = 1
 263        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 264
 265        See also: `GetCurrentPrices()`.
 266        """
 267
 268        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 269        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 270
 271        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 272        """
 273
 274        uLogger.debug("Broker API server: {}".format(self.server))
 275
 276        self.timeout = 15
 277        """Server operations timeout in seconds. Default: `15`.
 278
 279        See also: `SendAPIRequest()`.
 280        """
 281
 282        self.headers = {
 283            "Content-Type": "application/json",
 284            "accept": "application/json",
 285            "Authorization": "Bearer {}".format(self.token),
 286            "x-app-name": "Tim55667757.TKSBrokerAPI",
 287        }
 288        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 289
 290        See also: `SendAPIRequest()`.
 291        """
 292
 293        self.body = None
 294        """Request body which send to broker server. Default: `None`.
 295
 296        See also: `SendAPIRequest()`.
 297        """
 298
 299        self.historyFile = None
 300        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 301
 302        See also: `History()`.
 303        """
 304
 305        self.htmlHistoryFile = "index.html"
 306        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 307
 308        See also: `ShowHistoryChart()`.
 309        """
 310
 311        self.instrumentsFile = "instruments.md"
 312        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 313
 314        See also: `ShowInstrumentsInfo()`.
 315        """
 316
 317        self.searchResultsFile = "search-results.md"
 318        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 319
 320        See also: `SearchInstruments()`.
 321        """
 322
 323        self.pricesFile = "prices.md"
 324        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 325
 326        See also: `GetListOfPrices()`.
 327        """
 328
 329        self.infoFile = "info.md"
 330        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 331
 332        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 333        """
 334
 335        self.bondsXLSXFile = "ext-bonds.xlsx"
 336        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 337        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 338
 339        See also: `ExtendBondsData()`.
 340        """
 341
 342        self.calendarFile = "calendar.md"
 343        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 344        
 345        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 346
 347        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 348        """
 349
 350        self.overviewFile = "overview.md"
 351        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 352
 353        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 354        """
 355
 356        self.overviewDigestFile = "overview-digest.md"
 357        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 358
 359        See also: `Overview()` with parameter `details="digest"`.
 360        """
 361
 362        self.overviewPositionsFile = "overview-positions.md"
 363        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 364
 365        See also: `Overview()` with parameter `details="positions"`.
 366        """
 367
 368        self.overviewOrdersFile = "overview-orders.md"
 369        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 370
 371        See also: `Overview()` with parameter `details="orders"`.
 372        """
 373
 374        self.overviewAnalyticsFile = "overview-analytics.md"
 375        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 376
 377        See also: `Overview()` with parameter `details="analytics"`.
 378        """
 379
 380        self.reportFile = "deals.md"
 381        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 382
 383        See also: `Deals()`.
 384        """
 385
 386        self.withdrawalLimitsFile = "limits.md"
 387        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 388
 389        See also: `OverviewLimits()` and `RequestLimits()`.
 390        """
 391
 392        self.userInfoFile = "user-info.md"
 393        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 394
 395        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 396        """
 397
 398        self.userAccountsFile = "accounts.md"
 399        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 400
 401        See also: `OverviewAccounts()`, `RequestAccounts()`.
 402        """
 403
 404        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 405        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 406
 407        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 408
 409        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 410        """
 411
 412        self.iList = None  # init iList for raw instruments data
 413        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 414        
 415        See also: `Listing()`, `DumpInstruments()`.
 416        """
 417
 418        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 419        if useCache:
 420            if os.path.exists(self.iListDumpFile):
 421                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 422                curTime = datetime.now(tzutc())
 423
 424                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 425                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 426
 427                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 428
 429                else:
 430                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 431
 432                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 433                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 434
 435            else:
 436                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 437                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 438
 439        else:
 440            self.iList = self.Listing()  # request new raw instruments data from broker server
 441            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 442
 443        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 444        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 445
 446        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 447        """
 448
 449    @staticmethod
 450    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 451        """
 452        Parse JSON from response string.
 453
 454        :param rawData: this is a string with JSON-formatted text.
 455        :param debug: if `True` then print more debug information.
 456        :return: JSON (dictionary), parsed from server response string.
 457        """
 458        if debug:
 459            uLogger.debug("Raw text body:")
 460            uLogger.debug(rawData)
 461
 462        responseJSON = json.loads(rawData) if rawData else {}
 463
 464        if debug:
 465            uLogger.debug("JSON formatted:")
 466            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 467                uLogger.debug(jsonLine)
 468
 469        return responseJSON
 470
 471    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 472        """
 473        Send GET or POST request to broker server and receive JSON object.
 474
 475        self.header: must be defining with dictionary of headers.
 476        self.body: if define then used as request body. None by default.
 477        self.timeout: global request timeout, 15 seconds by default.
 478        :param url: url with REST request.
 479        :param reqType: send "GET" or "POST" request. "GET" by default.
 480        :param retry: how many times retry after first request if an 5xx server errors occurred.
 481        :param pause: sleep time in seconds between retries.
 482        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 483        :return: response JSON (dictionary) from broker.
 484        """
 485        if reqType not in ("GET", "POST"):
 486            uLogger.error("You can define request type: 'GET' or 'POST'!")
 487            raise Exception("Incorrect value")
 488
 489        if debug:
 490            uLogger.debug("Request parameters:")
 491            uLogger.debug("    - REST API URL: {}".format(url))
 492            uLogger.debug("    - request type: {}".format(reqType))
 493            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 494            uLogger.debug("    - body: {}".format(self.body))
 495
 496        # fast hack to avoid all operations with some tickers/FIGI
 497        responseJSON = {}
 498        oK = True
 499        for item in self.exclude:
 500            if item in url:
 501                if debug:
 502                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 503
 504                oK = False
 505                break
 506
 507        if oK:
 508            counter = 0
 509            response = None
 510            errMsg = ""
 511
 512            while not response and counter <= retry:
 513                if reqType == "GET":
 514                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 515
 516                if reqType == "POST":
 517                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 518
 519                if debug:
 520                    uLogger.debug("Response:")
 521                    uLogger.debug("    - status code: {}".format(response.status_code))
 522                    uLogger.debug("    - reason: {}".format(response.reason))
 523                    uLogger.debug("    - body length: {}".format(len(response.text)))
 524                    uLogger.debug("    - headers: {}".format(response.headers))
 525
 526                # Server returns some headers:
 527                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 528                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 529                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 530                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 531                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 532                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 533                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 534                    sleep(rateLimitWait)
 535
 536                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 537                if 400 <= response.status_code < 500:
 538                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 539                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 540                    counter = retry + 1
 541
 542                if 500 <= response.status_code < 600:
 543                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 544                    uLogger.debug("    - not oK, {}".format(errMsg))
 545                    counter += 1
 546
 547                    if counter <= retry:
 548                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 549                        sleep(pause)
 550
 551            responseJSON = self._ParseJSON(response.text)
 552
 553            if errMsg:
 554                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 555                uLogger.error("    - not oK, {}".format(errMsg))
 556
 557        return responseJSON
 558
 559    def _IUpdater(self, iType: str) -> tuple:
 560        """
 561        Request instrument by type from server. See available API methods for instruments:
 562        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 563        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 564        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 565        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 566        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 567
 568        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 569        :return: tuple with iType name and list of available instruments of current type for defined user token.
 570        """
 571        result = []
 572
 573        if iType in TKS_INSTRUMENTS:
 574            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 575
 576            # all instruments have the same body in API v2 requests:
 577            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 578            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 579            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 580
 581        return iType, result
 582
 583    def _IWrapper(self, kwargs):
 584        """
 585        Wrapper runs instrument's update method `_IUpdater()`.
 586        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 587        """
 588        return self._IUpdater(**kwargs)
 589
 590    def Listing(self) -> dict:
 591        """
 592        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 593
 594        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 595        """
 596        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 597        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 598
 599        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 600        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 601        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 602
 603        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 604        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 605        poolUpdater.close()
 606
 607        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 608        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 609        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 610
 611        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 612        for iType in iList.keys():
 613            for ticker in iList[iType]:
 614                iList[iType][ticker]["type"] = iType
 615
 616                if "minPriceIncrement" in iList[iType][ticker].keys():
 617                    iList[iType][ticker]["step"] = NanoToFloat(
 618                        iList[iType][ticker]["minPriceIncrement"]["units"],
 619                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 620                    )
 621
 622                else:
 623                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 624
 625        return iList
 626
 627    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 628        """
 629        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 630
 631        See also: `DumpInstruments()`, `Listing()`.
 632
 633        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 634                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 635        """
 636        if self.iListDumpFile is None or not self.iListDumpFile:
 637            uLogger.error("Output name of dump file must be defined!")
 638            raise Exception("Filename required")
 639
 640        if not self.iList or forceUpdate:
 641            self.iList = self.Listing()
 642
 643        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 644
 645        # Save as XLSX with separated sheets for every type of instruments:
 646        with pd.ExcelWriter(
 647                path=xlsxDumpFile,
 648                date_format=TKS_DATE_FORMAT,
 649                datetime_format=TKS_DATE_TIME_FORMAT,
 650                mode="w",
 651        ) as writer:
 652            for iType in TKS_INSTRUMENTS:
 653                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 654                df = df[sorted(df)]  # sorted by column names
 655                df = df.applymap(
 656                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 657                    na_action="ignore",
 658                )  # converting numbers from nano-type to float in every cell
 659                df.to_excel(
 660                    writer,
 661                    sheet_name=iType,
 662                    encoding="UTF-8",
 663                    freeze_panes=(1, 1),
 664                )  # saving as XLSX-file with freeze first row and column as headers
 665
 666        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 667
 668    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 669        """
 670        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 671        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 672
 673        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 674
 675        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 676                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 677        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 678        """
 679        if self.iListDumpFile is None or not self.iListDumpFile:
 680            uLogger.error("Output name of dump file must be defined!")
 681            raise Exception("Filename required")
 682
 683        if not self.iList or forceUpdate:
 684            self.iList = self.Listing()
 685
 686        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 687        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 688            fH.write(jsonDump)
 689
 690        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 691
 692        return jsonDump
 693
 694    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 695        """
 696        Show information about one instrument defined by json data and prints it in Markdown format.
 697
 698        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 699
 700        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 701        :param show: if `True` then also printing information about instrument and its current price.
 702        :return: multilines text in Markdown format with information about one instrument.
 703        """
 704        splitLine = "|                                                             |                                                        |\n"
 705        infoText = ""
 706
 707        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 708            info = [
 709                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 710                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 711                "| Parameters                                                  | Values                                                 |\n",
 712                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 713                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 714                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 715            ]
 716
 717            if "sector" in iJSON.keys() and iJSON["sector"]:
 718                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 719
 720            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 721                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 722                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 723            )))
 724
 725            info.extend([
 726                splitLine,
 727                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 728                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 729            ])
 730
 731            if "isin" in iJSON.keys() and iJSON["isin"]:
 732                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 733
 734            if "classCode" in iJSON.keys():
 735                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 736
 737            info.extend([
 738                splitLine,
 739                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 740                splitLine,
 741                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 742                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 743                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 744            ])
 745
 746            if iJSON["figi"]:
 747                self.figi = iJSON["figi"]
 748                iJSON = iJSON | self.RequestTradingStatus()
 749
 750                info.extend([
 751                    splitLine,
 752                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 753                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 754                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 755                ])
 756
 757            info.append(splitLine)
 758
 759            if "type" in iJSON.keys() and iJSON["type"]:
 760                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 761
 762            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 763                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 764
 765            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 766                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 767
 768            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 769                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 770
 771            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 772                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 773
 774            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 775                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 776
 777            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 778                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 779
 780            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 781                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 782
 783            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 784                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 785
 786            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 787                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 788
 789            if "currency" in iJSON.keys():
 790                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 791
 792            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 793                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 794
 795            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 796                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 797
 798            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 799                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 800
 801            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 802                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 803
 804            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 805                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 806
 807            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 808                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 809
 810            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 811                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 812
 813            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 814                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 815
 816            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 817                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 818
 819            iExt = None
 820            if iJSON["type"] == "Bonds":
 821                info.extend([
 822                    splitLine,
 823                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 824                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 825                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 826                        iJSON["nominal"]["currency"],
 827                    )),
 828                ])
 829
 830                if "floatingCouponFlag" in iJSON.keys():
 831                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 832
 833                if "amortizationFlag" in iJSON.keys():
 834                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 835
 836                info.append(splitLine)
 837
 838                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 839                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 840
 841                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 842
 843                info.extend([
 844                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 845                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 846                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 847                ])
 848
 849                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 850                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 851                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 852                        iJSON["aciValue"]["currency"]
 853                    )))
 854
 855            if "currentPrice" in iJSON.keys():
 856                info.append(splitLine)
 857
 858                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 859                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 860
 861                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 862                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 863                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 864                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 865                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 866
 867                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 868                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 869
 870                info.extend([
 871                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 872                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 873                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 874                    )),
 875                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 876                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 877                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 878                    )),
 879                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 880                        "{:.2f}%{}".format(
 881                            iJSON["currentPrice"]["changes"],
 882                            " ({}{:.2f} {})".format(
 883                                "+" if bondChangesDelta > 0 else "",
 884                                bondChangesDelta,
 885                                aciCurrency
 886                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 887                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 888                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 889                                currency
 890                            ),
 891                        )
 892                    ),
 893                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 894                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 896                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 897                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 898                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 899                    )),
 900                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 901                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 902                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 903                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 904                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 905                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 906                    )),
 907                ])
 908
 909            if "lot" in iJSON.keys():
 910                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 911
 912            if "step" in iJSON.keys() and iJSON["step"] != 0:
 913                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 914
 915            # Add bond payment calendar:
 916            if iJSON["type"] == "Bonds":
 917                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 918                info.extend(["\n", strCalendar])
 919
 920            infoText += "".join(info)
 921
 922            if show:
 923                uLogger.info("{}".format(infoText))
 924
 925            else:
 926                uLogger.debug("{}".format(infoText))
 927
 928            if self.infoFile is not None:
 929                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 930                    fH.write(infoText)
 931
 932                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 933
 934        return infoText
 935
 936    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 937        """
 938        Search and return raw broker's information about instrument by its ticker.
 939        `ticker` must be defined! If debug=True then print all debug messages.
 940
 941        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 942        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 943        :param debug: if `True` then print all debug console messages.
 944        :return: JSON formatted data with information about instrument.
 945        """
 946        tickerJSON = {}
 947        if debug:
 948            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 949
 950        if not self.ticker:
 951            uLogger.warning("self.ticker variable is not be empty!")
 952
 953        else:
 954            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 955                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 956                raise Exception("Instrument not allowed")
 957
 958            if not self.iList:
 959                self.iList = self.Listing()
 960
 961            if self.ticker in self.iList["Shares"].keys():
 962                tickerJSON = self.iList["Shares"][self.ticker]
 963                if debug:
 964                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 965
 966            elif self.ticker in self.iList["Currencies"].keys():
 967                tickerJSON = self.iList["Currencies"][self.ticker]
 968                if debug:
 969                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 970
 971            elif self.ticker in self.iList["Bonds"].keys():
 972                tickerJSON = self.iList["Bonds"][self.ticker]
 973                if debug:
 974                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 975
 976            elif self.ticker in self.iList["Etfs"].keys():
 977                tickerJSON = self.iList["Etfs"][self.ticker]
 978                if debug:
 979                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 980
 981            elif self.ticker in self.iList["Futures"].keys():
 982                tickerJSON = self.iList["Futures"][self.ticker]
 983                if debug:
 984                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 985
 986        if tickerJSON:
 987            self.figi = tickerJSON["figi"]
 988
 989            if requestPrice:
 990                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 991
 992                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 993                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 994
 995                else:
 996                    tickerJSON["currentPrice"]["changes"] = 0
 997
 998            if show:
 999                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1000
1001        else:
1002            if show:
1003                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1004
1005        return tickerJSON
1006
1007    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1008        """
1009        Search and return raw broker's information about instrument by its FIGI.
1010        `figi` must be defined! If debug=True then print all debug messages.
1011
1012        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1013        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1014        :param debug: if `True` then print all debug console messages.
1015        :return: JSON formatted data with information about instrument.
1016        """
1017        figiJSON = {}
1018        if debug:
1019            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1020
1021        if not self.figi:
1022            uLogger.warning("self.figi variable is not be empty!")
1023
1024        else:
1025            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1026                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1027                raise Exception("Instrument not allowed")
1028
1029            if not self.iList:
1030                self.iList = self.Listing()
1031
1032            for item in self.iList["Shares"].keys():
1033                if self.figi == self.iList["Shares"][item]["figi"]:
1034                    figiJSON = self.iList["Shares"][item]
1035
1036                    if debug:
1037                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1038
1039                    break
1040
1041            if not figiJSON:
1042                for item in self.iList["Currencies"].keys():
1043                    if self.figi == self.iList["Currencies"][item]["figi"]:
1044                        figiJSON = self.iList["Currencies"][item]
1045
1046                        if debug:
1047                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1048
1049                        break
1050
1051            if not figiJSON:
1052                for item in self.iList["Bonds"].keys():
1053                    if self.figi == self.iList["Bonds"][item]["figi"]:
1054                        figiJSON = self.iList["Bonds"][item]
1055
1056                        if debug:
1057                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1058
1059                        break
1060
1061            if not figiJSON:
1062                for item in self.iList["Etfs"].keys():
1063                    if self.figi == self.iList["Etfs"][item]["figi"]:
1064                        figiJSON = self.iList["Etfs"][item]
1065
1066                        if debug:
1067                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1068
1069                        break
1070
1071            if not figiJSON:
1072                for item in self.iList["Futures"].keys():
1073                    if self.figi == self.iList["Futures"][item]["figi"]:
1074                        figiJSON = self.iList["Futures"][item]
1075
1076                        if debug:
1077                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1078
1079                        break
1080
1081        if figiJSON:
1082            self.figi = figiJSON["figi"]
1083            self.ticker = figiJSON["ticker"]
1084
1085            if requestPrice:
1086                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1087
1088                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1089                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1090
1091                else:
1092                    figiJSON["currentPrice"]["changes"] = 0
1093
1094            if show:
1095                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1096
1097        else:
1098            if show:
1099                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1100
1101        return figiJSON
1102
1103    def GetCurrentPrices(self, show: bool = True) -> dict:
1104        """
1105        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1106        `{"buy": [{"price": 1243.8, "quantity": 193},
1107                  {"price": 1244.0, "quantity": 168},
1108                  {"price": 1244.8, "quantity": 5},
1109                  {"price": 1245.0, "quantity": 61},
1110                  {"price": 1245.4, "quantity": 60}],
1111          "sell": [{"price": 1243.6, "quantity": 8},
1112                   {"price": 1242.6, "quantity": 10},
1113                   {"price": 1242.4, "quantity": 18},
1114                   {"price": 1242.2, "quantity": 50},
1115                   {"price": 1242.0, "quantity": 113}],
1116          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1117        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1118        - sell: list of dicts with Buyers prices,
1119            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1120            - quantity: volume value by current price in lots,
1121        - limitUp: current trade session limit price, maximum,
1122        - limitDown: current trade session limit price, minimum,
1123        - lastPrice: last deal price of the instrument,
1124        - closePrice: previous trade session close price of the instrument.
1125
1126        See also: `SearchByTicker()` and `SearchByFIGI()`.
1127        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1128        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1129
1130        :param show: if `True` then print DOM to log and console.
1131        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1132                 If an error occurred then returns an empty record:
1133                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1134        """
1135        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1136
1137        if self.depth < 1:
1138            uLogger.error("Depth of Market (DOM) must be >=1!")
1139            raise Exception("Incorrect value")
1140
1141        if not (self.ticker or self.figi):
1142            uLogger.error("self.ticker or self.figi variables must be defined!")
1143            raise Exception("Ticker or FIGI required")
1144
1145        if self.ticker and not self.figi:
1146            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1147            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1148
1149        if not self.ticker and self.figi:
1150            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1151            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1152
1153        if not self.figi:
1154            uLogger.error("FIGI is not defined!")
1155            raise Exception("Ticker or FIGI required")
1156
1157        else:
1158            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1159
1160            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1161            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1162            self.body = str({"figi": self.figi, "depth": self.depth})
1163            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1164
1165            if pricesResponse:
1166                # list of dicts with sellers orders:
1167                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1168
1169                # list of dicts with buyers orders:
1170                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1171
1172                # max price of instrument at this time:
1173                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1174
1175                # min price of instrument at this time:
1176                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1177
1178                # last price of deal with instrument:
1179                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1180
1181                # last close price of instrument:
1182                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1183
1184            else:
1185                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1186                uLogger.debug("Server response: {}".format(pricesResponse))
1187
1188            if show:
1189                if prices["buy"] or prices["sell"]:
1190                    info = [
1191                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1192                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1193                            self.ticker,
1194                            self.figi,
1195                            self.depth,
1196                        ),
1197                        "-" * 60, "\n",
1198                        "             Orders of Buyers | Orders of Sellers\n",
1199                        "-" * 60, "\n",
1200                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1201                        "-" * 60, "\n",
1202                    ]
1203
1204                    if not prices["buy"]:
1205                        info.append("                              | No orders!\n")
1206                        sumBuy = 0
1207
1208                    else:
1209                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1210                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1211                        for item in maxMinSorted:
1212                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1213
1214                    if not prices["sell"]:
1215                        info.append("No orders!                    |\n")
1216                        sumSell = 0
1217
1218                    else:
1219                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1220                        for item in prices["sell"]:
1221                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1222
1223                    info.extend([
1224                        "-" * 60, "\n",
1225                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1226                        "-" * 60, "\n",
1227                    ])
1228
1229                    infoText = "".join(info)
1230
1231                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1232
1233                else:
1234                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1235
1236        return prices
1237
1238    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1239        """
1240        This method get and show information about all available broker instruments for current user account.
1241        If `instrumentsFile` string is not empty then also save information to this file.
1242
1243        :param show: if `True` then print results to console, if `False` - print only to file.
1244        :return: multi-lines string with all available broker instruments
1245        """
1246        if not self.iList:
1247            self.iList = self.Listing()
1248
1249        info = [
1250            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1251            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1252        ]
1253
1254        # add instruments count by type:
1255        for iType in self.iList.keys():
1256            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1257
1258        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1259        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1260
1261        # generating info tables with all instruments by type:
1262        for iType in self.iList.keys():
1263            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1264
1265            for instrument in self.iList[iType].keys():
1266                iName = self.iList[iType][instrument]["name"]  # instrument's name
1267                if len(iName) > 57:
1268                    iName = "{}...".format(iName[:54])  # right trim for a long string
1269
1270                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1271                    self.iList[iType][instrument]["ticker"],
1272                    iName,
1273                    self.iList[iType][instrument]["figi"],
1274                    self.iList[iType][instrument]["currency"],
1275                    self.iList[iType][instrument]["lot"],
1276                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1277                ))
1278
1279        infoText = "".join(info)
1280
1281        if show:
1282            uLogger.info(infoText)
1283
1284        if self.instrumentsFile:
1285            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1286                fH.write(infoText)
1287
1288            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1289
1290        return infoText
1291
1292    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1293        """
1294        This method search and show information about instruments by part of its ticker, FIGI or name.
1295        If `searchResultsFile` string is not empty then also save information to this file.
1296
1297        :param pattern: string with part of ticker, FIGI or instrument's name.
1298        :param show: if `True` then print results to console, if `False` - return list of result only.
1299        :return: list of dictionaries with all found instruments.
1300        """
1301        if not self.iList:
1302            self.iList = self.Listing()
1303
1304        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1305        compiledPattern = re.compile(pattern, re.IGNORECASE)
1306
1307        for iType in self.iList:
1308            for instrument in self.iList[iType].values():
1309                searchResult = compiledPattern.search(" ".join(
1310                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1311                ))
1312
1313                if searchResult:
1314                    searchResults[iType][instrument["ticker"]] = instrument
1315
1316        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1317        info = [
1318            "# Search results\n\n",
1319            "* **Search pattern:** [{}]\n".format(pattern),
1320            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1321            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1322        ]
1323        infoShort = info[:]
1324
1325        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1326        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1327        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1328
1329        if resultsLen == 0:
1330            info.append("\nNo results\n")
1331            infoShort.append("\nNo results\n")
1332            uLogger.warning("No results. Try changing your search pattern.")
1333
1334        else:
1335            for iType in searchResults:
1336                iTypeValuesCount = len(searchResults[iType].values())
1337                if iTypeValuesCount > 0:
1338                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1339                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1340
1341                    for instrument in searchResults[iType].values():
1342                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1343                            instrument["type"],
1344                            instrument["ticker"],
1345                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1346                            instrument["figi"],
1347                        ))
1348
1349                    if iTypeValuesCount <= 5:
1350                        infoShort.extend(info[-iTypeValuesCount:])
1351
1352                    else:
1353                        infoShort.extend(info[-5:])
1354                        infoShort.append(skippedLine)
1355
1356        infoText = "".join(info)
1357        infoTextShort = "".join(infoShort)
1358
1359        if show:
1360            uLogger.info(infoTextShort)
1361            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1362
1363        if self.searchResultsFile:
1364            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1365                fH.write(infoText)
1366
1367            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1368
1369        return searchResults
1370
1371    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1372        """
1373        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1374
1375        :param instruments: list of strings with tickers or FIGIs.
1376        :return: list with unique instrument FIGIs only.
1377        """
1378        requestedInstruments = []
1379        for iName in instruments:
1380            if iName not in self.aliases.keys():
1381                if iName not in requestedInstruments:
1382                    requestedInstruments.append(iName)
1383
1384            else:
1385                if iName not in requestedInstruments:
1386                    if self.aliases[iName] not in requestedInstruments:
1387                        requestedInstruments.append(self.aliases[iName])
1388
1389        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1390
1391        onlyUniqueFIGIs = []
1392        for iName in requestedInstruments:
1393            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1394                continue
1395
1396            self.ticker = iName
1397            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1398
1399            if not iData:
1400                self.ticker = ""
1401                self.figi = iName
1402
1403                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1404
1405                if not iData:
1406                    self.figi = ""
1407                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1408
1409            if iData and iData["figi"] not in onlyUniqueFIGIs:
1410                onlyUniqueFIGIs.append(iData["figi"])
1411
1412        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1413
1414        return onlyUniqueFIGIs
1415
1416    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1417        """
1418        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1419        See limits: https://tinkoff.github.io/investAPI/limits/
1420        If `pricesFile` string is not empty then also save information to this file.
1421
1422        :param instruments: list of strings with tickers or FIGIs.
1423        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1424        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1425                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1426        """
1427        if instruments is None or not instruments:
1428            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1429            raise Exception("Ticker or FIGI required")
1430
1431        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1432
1433        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1434
1435        iList = []  # trying to get info and current prices about all unique instruments:
1436        for self.figi in onlyUniqueFIGIs:
1437            iData = self.SearchByFIGI(requestPrice=True)
1438            iList.append(iData)
1439
1440        self.ShowListOfPrices(iList, show)
1441
1442        return iList
1443
1444    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1445        """
1446        Show table contains current prices of given instruments.
1447
1448        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1449                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1450        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1451        :return: multilines text in Markdown format as a table contains current prices.
1452        """
1453        infoText = ""
1454
1455        if show or self.pricesFile:
1456            info = [
1457                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1458                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1459                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1460            ]
1461
1462            for item in iList:
1463                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1464                    item["ticker"],
1465                    item["figi"],
1466                    item["type"],
1467                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1468                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1469                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1470                    "{} / {}".format(
1471                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1472                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1473                    ),
1474                    "{} / {}".format(
1475                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1476                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1477                    ),
1478                    item["currency"],
1479                ))
1480
1481            infoText = "".join(info)
1482
1483            if show:
1484                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1485
1486            if self.pricesFile:
1487                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1488                    fH.write(infoText)
1489
1490                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1491
1492        return infoText
1493
1494    def RequestTradingStatus(self) -> dict:
1495        """
1496        Requesting trading status for the instrument defined by `figi` variable.
1497        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1498        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1499
1500        :return: dictionary with trading status attributes. Response example:
1501                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1502                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1503        """
1504        if self.figi is None or not self.figi:
1505            uLogger.error("Variable `figi` must be defined for using this method!")
1506            raise Exception("FIGI required")
1507
1508        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1509
1510        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1511        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1512        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1513
1514        uLogger.debug("Records about current trading status successfully received")
1515
1516        return tradingStatus
1517
1518    def RequestPortfolio(self) -> dict:
1519        """
1520        Requesting actual user's portfolio for current `accountId`.
1521        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1522        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1523
1524        :return: dictionary with user's portfolio.
1525        """
1526        if self.accountId is None or not self.accountId:
1527            uLogger.error("Variable `accountId` must be defined for using this method!")
1528            raise Exception("Account ID required")
1529
1530        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1531
1532        self.body = str({"accountId": self.accountId})
1533        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1534        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1535
1536        uLogger.debug("Records about user's portfolio successfully received")
1537
1538        return rawPortfolio
1539
1540    def RequestPositions(self) -> dict:
1541        """
1542        Requesting open positions by currencies and instruments for current `accountId`.
1543        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1544        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1545
1546        :return: dictionary with open positions by instruments.
1547        """
1548        if self.accountId is None or not self.accountId:
1549            uLogger.error("Variable `accountId` must be defined for using this method!")
1550            raise Exception("Account ID required")
1551
1552        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1553
1554        self.body = str({"accountId": self.accountId})
1555        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1556        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1557
1558        uLogger.debug("Records about current open positions successfully received")
1559
1560        return rawPositions
1561
1562    def RequestPendingOrders(self) -> list:
1563        """
1564        Requesting current actual pending orders for current `accountId`.
1565        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1566        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1567
1568        :return: list of dictionaries with pending orders.
1569        """
1570        if self.accountId is None or not self.accountId:
1571            uLogger.error("Variable `accountId` must be defined for using this method!")
1572            raise Exception("Account ID required")
1573
1574        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1575
1576        self.body = str({"accountId": self.accountId})
1577        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1578        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1579
1580        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1581
1582        return rawOrders
1583
1584    def RequestStopOrders(self) -> list:
1585        """
1586        Requesting current actual stop orders for current `accountId`.
1587        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1588        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1589
1590        :return: list of dictionaries with stop orders.
1591        """
1592        if self.accountId is None or not self.accountId:
1593            uLogger.error("Variable `accountId` must be defined for using this method!")
1594            raise Exception("Account ID required")
1595
1596        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1597
1598        self.body = str({"accountId": self.accountId})
1599        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1600        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1601
1602        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1603
1604        return rawStopOrders
1605
1606    def Overview(self, show: bool = False, details: str = "full") -> dict:
1607        """
1608        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1609        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1610        are defined then also save information to file.
1611
1612        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1613        many requests about the state of the portfolio, and then, based on the received data, a large number
1614        of calculation and statistics are collected.
1615
1616        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1617        :param details: how detailed should the information be? You should specify one of strings:
1618                        `full` - shows full available information about portfolio status (by default),
1619                        `positions` - shows only open positions,
1620                        `digest` - show a short digest of the portfolio status,
1621                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1622                        `orders` - shows only sections of open limits and stop orders.
1623        :return: dictionary with client's raw portfolio and some statistics.
1624        """
1625        if self.accountId is None or not self.accountId:
1626            uLogger.error("Variable `accountId` must be defined for using this method!")
1627            raise Exception("Account ID required")
1628
1629        view = {
1630            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1631                "headers": {},  # list of dictionaries, response headers without "positions" section
1632                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1633                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1634                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1635                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1636                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1637                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1638                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1639                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1640                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1641            },
1642            "stat": {  # --- some statistics calculated using "raw" sections:
1643                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1644                "availableRUB": 0.,  # available rubles (without other currencies)
1645                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1646                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1647                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1648                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1649                "sharesCostRUB": 0.,  # costs of all shares in RUB
1650                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1651                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1652                "futuresCostRUB": 0.,  # costs of all futures in RUB
1653                "Currencies": [],  # list of dictionaries of all currencies statistics
1654                "Shares": [],  # list of dictionaries of all shares statistics
1655                "Bonds": [],  # list of dictionaries of all bonds statistics
1656                "Etfs": [],  # list of dictionaries of all etfs statistics
1657                "Futures": [],  # list of dictionaries of all futures statistics
1658                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1659                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1660                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1661                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1662                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1663            },
1664            "analytics": {  # --- some analytics of portfolio:
1665                "distrByAssets": {},  # portfolio distribution by assets
1666                "distrByCompanies": {},  # portfolio distribution by companies
1667                "distrBySectors": {},  # portfolio distribution by sectors
1668                "distrByCurrencies": {},  # portfolio distribution by currencies
1669                "distrByCountries": {},  # portfolio distribution by countries
1670            }
1671        }
1672
1673        details = details.lower()
1674        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1675        if details not in availableDetails:
1676            details = "full"
1677            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1678
1679        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1680
1681        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1682        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1683        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1684        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1685
1686        # save response headers without "positions" section:
1687        for key in portfolioResponse.keys():
1688            if key != "positions":
1689                view["raw"]["headers"][key] = portfolioResponse[key]
1690
1691            else:
1692                continue
1693
1694        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1695        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1696        for item in portfolioResponse["positions"]:
1697            if item["instrumentType"] == "currency":
1698                self.figi = item["figi"]
1699                curr = self.SearchByFIGI(requestPrice=False)
1700
1701                # current price of currency in RUB:
1702                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1703                    "name": curr["name"],
1704                    "currentPrice": NanoToFloat(
1705                        item["currentPrice"]["units"],
1706                        item["currentPrice"]["nano"]
1707                    ),
1708                }
1709
1710                view["raw"]["Currencies"].append(item)
1711
1712            elif item["instrumentType"] == "share":
1713                view["raw"]["Shares"].append(item)
1714
1715            elif item["instrumentType"] == "bond":
1716                view["raw"]["Bonds"].append(item)
1717
1718            elif item["instrumentType"] == "etf":
1719                view["raw"]["Etfs"].append(item)
1720
1721            elif item["instrumentType"] == "futures":
1722                view["raw"]["Futures"].append(item)
1723
1724            else:
1725                continue
1726
1727        # how many volume of currencies (by ISO currency name) are blocked:
1728        for item in view["raw"]["positions"]["blocked"]:
1729            blocked = NanoToFloat(item["units"], item["nano"])
1730            if blocked > 0:
1731                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1732
1733        # how many volume of instruments (by FIGI) are blocked:
1734        for item in view["raw"]["positions"]["securities"]:
1735            blocked = int(item["blocked"])
1736            if blocked > 0:
1737                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1738
1739        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1740
1741        if "rub" in allBlocked.keys():
1742            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1743
1744        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1745        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1746        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1747        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1748        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1749        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1750        view["stat"]["portfolioCostRUB"] = sum([
1751            view["stat"]["allCurrenciesCostRUB"],
1752            view["stat"]["sharesCostRUB"],
1753            view["stat"]["bondsCostRUB"],
1754            view["stat"]["etfsCostRUB"],
1755            view["stat"]["futuresCostRUB"],
1756        ])
1757
1758        # --- calculating some portfolio statistics:
1759        byComp = {}  # distribution by companies
1760        bySect = {}  # distribution by sectors
1761        byCurr = {}  # distribution by currencies (include RUB)
1762        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1763        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1764
1765        for item in portfolioResponse["positions"]:
1766            self.figi = item["figi"]
1767            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1768
1769            if instrument:
1770                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1771                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1772
1773                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1774                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1775
1776                else:
1777                    blocked = 0
1778
1779                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1780                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1781                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1782                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1783                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1784                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1785                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1786                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1787                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1788                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1789                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1790                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1791
1792                statData = {
1793                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1794                    "ticker": instrument["ticker"],  # ticker by FIGI
1795                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1796                    "volume": volume,  # available volume of instrument
1797                    "lots": lots,  # volume in lots of instrument
1798                    "direction": direction,  # direction of an instrument's position: short or long
1799                    "blocked": blocked,  # blocked volume of currency or instrument
1800                    "currentPrice": curPrice,  # current instrument's price in basic asset
1801                    "average": average,  # current average position price
1802                    "cost": cost,  # current cost of all volume of instrument in basic asset
1803                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1804                    "costRUB": costRUB,  # cost of instrument in ruble
1805                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1806                    "profit": profit,  # expected profit at current moment
1807                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1808                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1809                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1810                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1811                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1812                    "step": instrument["step"],  # minimum price increment
1813                }
1814
1815                # adding distribution by unique countries:
1816                if statData["country"] not in byCountry.keys():
1817                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1818
1819                else:
1820                    byCountry[statData["country"]]["cost"] += costRUB
1821                    byCountry[statData["country"]]["percent"] += percentCostRUB
1822
1823                if item["instrumentType"] != "currency":
1824                    # adding distribution by unique companies:
1825                    if statData["name"]:
1826                        if statData["name"] not in byComp.keys():
1827                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1828
1829                        else:
1830                            byComp[statData["name"]]["cost"] += costRUB
1831                            byComp[statData["name"]]["percent"] += percentCostRUB
1832
1833                    # adding distribution by unique sectors:
1834                    if statData["sector"] not in bySect.keys():
1835                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1836
1837                    else:
1838                        bySect[statData["sector"]]["cost"] += costRUB
1839                        bySect[statData["sector"]]["percent"] += percentCostRUB
1840
1841                # adding distribution by unique currencies:
1842                if currency not in byCurr.keys():
1843                    byCurr[currency] = {
1844                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1845                        "cost": costRUB,
1846                        "percent": percentCostRUB
1847                    }
1848
1849                else:
1850                    byCurr[currency]["cost"] += costRUB
1851                    byCurr[currency]["percent"] += percentCostRUB
1852
1853                # saving statistics for every instrument:
1854                if item["instrumentType"] == "currency":
1855                    view["stat"]["Currencies"].append(statData)
1856
1857                    # update dict with free funds for trading (total - blocked) by currencies
1858                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1859                    view["stat"]["funds"][currency] = {
1860                        "total": volume,
1861                        "totalCostRUB": costRUB,  # total volume cost in rubles
1862                        "free": volume - blocked,
1863                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1864                    }
1865
1866                elif item["instrumentType"] == "share":
1867                    view["stat"]["Shares"].append(statData)
1868
1869                elif item["instrumentType"] == "bond":
1870                    view["stat"]["Bonds"].append(statData)
1871
1872                elif item["instrumentType"] == "etf":
1873                    view["stat"]["Etfs"].append(statData)
1874
1875                elif item["instrumentType"] == "Futures":
1876                    view["stat"]["Futures"].append(statData)
1877
1878                else:
1879                    continue
1880
1881        # total changes in Russian Ruble:
1882        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1883        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1884        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1885        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1886        view["stat"]["funds"]["rub"] = {
1887            "total": view["stat"]["availableRUB"],
1888            "totalCostRUB": view["stat"]["availableRUB"],
1889            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1890            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1891        }
1892
1893        # --- pending orders sector data:
1894        uniquePendingOrders = []
1895        uniquePendingOrdersFIGIs = []
1896        for item in view["raw"]["orders"]:
1897            if item["figi"] not in uniquePendingOrdersFIGIs:
1898                uniquePendingOrdersFIGIs.append(item["figi"])
1899                uniquePendingOrders.append(item)
1900
1901        for item in uniquePendingOrders:
1902            self.figi = item["figi"]
1903            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1904
1905            if instrument:
1906                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1907                orderType = TKS_ORDER_TYPES[item["orderType"]]
1908                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1909                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1910
1911                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1912                if item["direction"] == "ORDER_DIRECTION_BUY":
1913                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1914
1915                else:
1916                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1917
1918                # requested price for order execution:
1919                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1920
1921                # necessary changes in percent to reach target from current price:
1922                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1923
1924                view["stat"]["orders"].append({
1925                    "orderID": item["orderId"],  # orderId number parameter of current order
1926                    "figi": item["figi"],  # FIGI identification
1927                    "ticker": instrument["ticker"],  # ticker name by FIGI
1928                    "lotsRequested": item["lotsRequested"],  # requested lots value
1929                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1930                    "currentPrice": lastPrice,  # current instrument's price for defined action
1931                    "targetPrice": target,  # requested price for order execution in base currency
1932                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1933                    "percentChanges": changes,  # changes in percent to target from current price
1934                    "currency": item["currency"],  # instrument's currency name
1935                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1936                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1937                    "status": orderState,  # order status from TKS_ORDER_STATES
1938                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1939                })
1940
1941        # --- stop orders sector data:
1942        uniqueStopOrders = []
1943        uniqueStopOrdersFIGIs = []
1944        for item in view["raw"]["stopOrders"]:
1945            if item["figi"] not in uniqueStopOrdersFIGIs:
1946                uniqueStopOrdersFIGIs.append(item["figi"])
1947                uniqueStopOrders.append(item)
1948
1949        for item in uniqueStopOrders:
1950            self.figi = item["figi"]
1951            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1952
1953            if instrument:
1954                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1955                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1956                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1957
1958                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1959                if "expirationTime" in item.keys():
1960                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1961                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1962
1963                else:
1964                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1965                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1966
1967                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1968                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1969                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1970
1971                else:
1972                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1973
1974                # requested price when stop-order executed:
1975                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1976
1977                # price for limit-order, set up when stop-order executed:
1978                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1979
1980                # necessary changes in percent to reach target from current price:
1981                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1982
1983                view["stat"]["stopOrders"].append({
1984                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1985                    "figi": item["figi"],  # FIGI identification
1986                    "ticker": instrument["ticker"],  # ticker name by FIGI
1987                    "lotsRequested": item["lotsRequested"],  # requested lots value
1988                    "currentPrice": lastPrice,  # current instrument's price for defined action
1989                    "targetPrice": target,  # requested price for stop-order execution in base currency
1990                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1991                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1992                    "percentChanges": changes,  # changes in percent to target from current price
1993                    "currency": item["currency"],  # instrument's currency name
1994                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1995                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1996                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1997                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1998                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1999                })
2000
2001        # --- calculating data for analytics section:
2002        # portfolio distribution by assets:
2003        view["analytics"]["distrByAssets"] = {
2004            "Ruble": {
2005                "uniques": 1,
2006                "cost": view["stat"]["availableRUB"],
2007                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2008            },
2009            "Currencies": {
2010                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2011                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2012                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2013            },
2014            "Shares": {
2015                "uniques": len(view["stat"]["Shares"]),
2016                "cost": view["stat"]["sharesCostRUB"],
2017                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2018            },
2019            "Bonds": {
2020                "uniques": len(view["stat"]["Bonds"]),
2021                "cost": view["stat"]["bondsCostRUB"],
2022                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2023            },
2024            "Etfs": {
2025                "uniques": len(view["stat"]["Etfs"]),
2026                "cost": view["stat"]["etfsCostRUB"],
2027                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2028            },
2029            "Futures": {
2030                "uniques": len(view["stat"]["Futures"]),
2031                "cost": view["stat"]["futuresCostRUB"],
2032                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2033            },
2034        }
2035
2036        # portfolio distribution by companies:
2037        view["analytics"]["distrByCompanies"]["All money cash"] = {
2038            "ticker": "",
2039            "cost": view["stat"]["allCurrenciesCostRUB"],
2040            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2041        }
2042        view["analytics"]["distrByCompanies"].update(byComp)
2043
2044        # portfolio distribution by sectors:
2045        view["analytics"]["distrBySectors"]["All money cash"] = {
2046            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2047            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2048        }
2049        view["analytics"]["distrBySectors"].update(bySect)
2050
2051        # portfolio distribution by currencies:
2052        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2053            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2054            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2055
2056        view["analytics"]["distrByCurrencies"].update(byCurr)
2057        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2058        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2059
2060        # portfolio distribution by countries:
2061        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2062            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2063            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2064
2065        view["analytics"]["distrByCountries"].update(byCountry)
2066        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2067        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2068
2069        # --- Prepare text statistics overview in human-readable:
2070        if show:
2071            # Whatever the value `details`, header not changes:
2072            info = [
2073                "# Client's portfolio\n\n",
2074                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2075                "* **Account ID:** [{}]\n".format(self.accountId),
2076            ]
2077
2078            if details in ["full", "positions", "digest"]:
2079                info.extend([
2080                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2081                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2082                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2083                        view["stat"]["totalChangesRUB"],
2084                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2085                        view["stat"]["totalChangesPercentRUB"],
2086                    ),
2087                ])
2088
2089            if details in ["full", "positions"]:
2090                info.extend([
2091                    "## Open positions\n\n",
2092                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2093                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2094                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2095                        "{:.2f} ({:.2f}) rub".format(
2096                            view["stat"]["availableRUB"],
2097                            view["stat"]["blockedRUB"],
2098                        )
2099                    )
2100                ])
2101
2102                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2103                    return [
2104                        "|                             |                                 |          |              |              |                     |                              |\n",
2105                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2106                            noTradeStr if noTradeStr else typeStr,
2107                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2108                        ),
2109                    ]
2110
2111                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2112                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2113                        "{} [{}]".format(data["ticker"], data["figi"]),
2114                        "{:.2f} ({:.2f}) {}".format(
2115                            data["volume"],
2116                            data["blocked"],
2117                            data["currency"],
2118                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2119                            data["volume"],
2120                            data["blocked"],
2121                        ),
2122                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2123                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2124                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2125                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2126                        "{}{:.2f} {} ({}{:.2f}%)".format(
2127                            "+" if data["profit"] > 0 else "",
2128                            data["profit"], data["baseCurrencyName"],
2129                            "+" if data["percentProfit"] > 0 else "",
2130                            data["percentProfit"],
2131                        ),
2132                    )
2133
2134                # --- Show currencies section:
2135                if view["stat"]["Currencies"]:
2136                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2137                    for item in view["stat"]["Currencies"]:
2138                        info.append(_InfoStr(item, showCurrencyName=True))
2139
2140                else:
2141                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2142
2143                # --- Show shares section:
2144                if view["stat"]["Shares"]:
2145                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2146
2147                    for item in view["stat"]["Shares"]:
2148                        info.append(_InfoStr(item))
2149
2150                else:
2151                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2152
2153                # --- Show bonds section:
2154                if view["stat"]["Bonds"]:
2155                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2156
2157                    for item in view["stat"]["Bonds"]:
2158                        info.append(_InfoStr(item))
2159
2160                else:
2161                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2162
2163                # --- Show etfs section:
2164                if view["stat"]["Etfs"]:
2165                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2166
2167                    for item in view["stat"]["Etfs"]:
2168                        info.append(_InfoStr(item))
2169
2170                else:
2171                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2172
2173                # --- Show futures section:
2174                if view["stat"]["Futures"]:
2175                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2176
2177                    for item in view["stat"]["Futures"]:
2178                        info.append(_InfoStr(item))
2179
2180                else:
2181                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2182
2183            if details in ["full", "orders"]:
2184                # --- Show pending orders section:
2185                if view["stat"]["orders"]:
2186                    info.extend([
2187                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2188                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2189                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2190                    ])
2191
2192                    for item in view["stat"]["orders"]:
2193                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2194                            "{} [{}]".format(item["ticker"], item["figi"]),
2195                            item["orderID"],
2196                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2197                            "{} {} ({}{:.2f}%)".format(
2198                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2199                                item["baseCurrencyName"],
2200                                "+" if item["percentChanges"] > 0 else "",
2201                                float(item["percentChanges"]),
2202                            ),
2203                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2204                            item["action"],
2205                            item["type"],
2206                            item["date"],
2207                        ))
2208
2209                else:
2210                    info.append("\n## Total pending limit-orders: 0\n")
2211
2212                # --- Show stop orders section:
2213                if view["stat"]["stopOrders"]:
2214                    info.extend([
2215                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2216                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2217                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2218                    ])
2219
2220                    for item in view["stat"]["stopOrders"]:
2221                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2222                            "{} [{}]".format(item["ticker"], item["figi"]),
2223                            item["orderID"],
2224                            item["lotsRequested"],
2225                            "{} {} ({}{:.2f}%)".format(
2226                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2227                                item["baseCurrencyName"],
2228                                "+" if item["percentChanges"] > 0 else "",
2229                                float(item["percentChanges"]),
2230                            ),
2231                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2232                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2233                            item["action"],
2234                            item["type"],
2235                            item["expType"],
2236                            item["createDate"],
2237                            item["expDate"],
2238                        ))
2239
2240                else:
2241                    info.append("\n## Total stop-orders: 0\n")
2242
2243            if details in ["full", "analytics"]:
2244                # -- Show analytics section:
2245                if view["stat"]["portfolioCostRUB"] > 0:
2246                    info.extend([
2247                        "\n# Analytics\n"
2248                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2249                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2250                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2251                            view["stat"]["totalChangesRUB"],
2252                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2253                            view["stat"]["totalChangesPercentRUB"],
2254                        ),
2255                        "\n## Portfolio distribution by assets\n"
2256                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2257                        "|------------|---------|---------|--------------------|\n",
2258                    ])
2259
2260                    for key in view["analytics"]["distrByAssets"].keys():
2261                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2262                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2263                                key,
2264                                view["analytics"]["distrByAssets"][key]["uniques"],
2265                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2266                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2267                            ))
2268
2269                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2270                    info.extend([
2271                        "\n## Portfolio distribution by companies\n"
2272                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2273                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2274                    ])
2275
2276                    for company in view["analytics"]["distrByCompanies"].keys():
2277                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2278                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2279                            info.append("| {} | {:<7} | {:<18} |\n".format(
2280                                "{}{}{}".format(
2281                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2282                                    company,
2283                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2284                                ),
2285                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2286                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2287                            ))
2288
2289                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2290                    info.extend([
2291                        "\n## Portfolio distribution by sectors\n"
2292                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2293                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2294                    ])
2295
2296                    for sector in view["analytics"]["distrBySectors"].keys():
2297                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2298                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2299                                sector,
2300                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2301                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2302                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2303                            ))
2304
2305                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2306                    info.extend([
2307                        "\n## Portfolio distribution by currencies\n"
2308                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2309                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2310                    ])
2311
2312                    for curr in view["analytics"]["distrByCurrencies"].keys():
2313                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2314                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2315                            info.append("| {} | {:<7} | {:<18} |\n".format(
2316                                "[{}] {}{}".format(
2317                                    curr,
2318                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2319                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2320                                ),
2321                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2322                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2323                            ))
2324
2325                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2326                    info.extend([
2327                        "\n## Portfolio distribution by countries\n"
2328                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2329                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2330                    ])
2331
2332                    for country in view["analytics"]["distrByCountries"].keys():
2333                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2334                            nameLen = len(country)
2335                            info.append("| {} | {:<7} | {:<18} |\n".format(
2336                                "{}{}".format(
2337                                    country,
2338                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2339                                ),
2340                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2341                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2342                            ))
2343
2344            infoText = "".join(info)
2345
2346            uLogger.info(infoText)
2347
2348            if details == "full" and self.overviewFile:
2349                filename = self.overviewFile
2350
2351            elif details == "digest" and self.overviewDigestFile:
2352                filename = self.overviewDigestFile
2353
2354            elif details == "positions" and self.overviewPositionsFile:
2355                filename = self.overviewPositionsFile
2356
2357            elif details == "orders" and self.overviewOrdersFile:
2358                filename = self.overviewOrdersFile
2359
2360            elif details == "analytics" and self.overviewAnalyticsFile:
2361                filename = self.overviewAnalyticsFile
2362
2363            else:
2364                filename = ""
2365
2366            if filename:
2367                with open(filename, "w", encoding="UTF-8") as fH:
2368                    fH.write(infoText)
2369
2370                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2371
2372        return view
2373
2374    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2375        """
2376        Returns history operations between two given dates for current `accountId`.
2377        If `reportFile` string is not empty then also save human-readable report.
2378        Shows some statistical data of closed positions.
2379
2380        :param start: see docstring in `GetDatesAsString()` method
2381        :param end: see docstring in `GetDatesAsString()` method
2382        :param show: if `True` then also prints all records to the console.
2383        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2384        :return: original list of dictionaries with history of deals records from API ("operations" key):
2385                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2386                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2387        """
2388        if self.accountId is None or not self.accountId:
2389            uLogger.error("Variable `accountId` must be defined for using this method!")
2390            raise Exception("Account ID required")
2391
2392        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2393
2394        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2395
2396        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2397        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2398        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2399        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2400        customStat = {}  # custom statistics in additional to responseJSON
2401
2402        # --- output report in human-readable format:
2403        if show or self.reportFile:
2404            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2405            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2406            nextDay = ""
2407
2408            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2409
2410            if len(ops) > 0:
2411                customStat = {
2412                    "opsCount": 0,  # total operations count
2413                    "buyCount": 0,  # buy operations
2414                    "sellCount": 0,  # sell operations
2415                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2416                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2417                    "payIn": {"rub": 0.},  # Deposit brokerage account
2418                    "payOut": {"rub": 0.},  # Withdrawals
2419                    "divs": {"rub": 0.},  # Dividends income
2420                    "coupons": {"rub": 0.},  # Coupon's income
2421                    "brokerCom": {"rub": 0.},  # Service commissions
2422                    "serviceCom": {"rub": 0.},  # Service commissions
2423                    "marginCom": {"rub": 0.},  # Margin commissions
2424                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2425                }
2426
2427                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2428                for item in ops:
2429                    if item["state"] == "OPERATION_STATE_EXECUTED":
2430                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2431
2432                        # count buy operations:
2433                        if "_BUY" in item["operationType"]:
2434                            customStat["buyCount"] += 1
2435
2436                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2437                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2438
2439                            else:
2440                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2441
2442                        # count sell operations:
2443                        elif "_SELL" in item["operationType"]:
2444                            customStat["sellCount"] += 1
2445
2446                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2447                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2448
2449                            else:
2450                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2451
2452                        # count incoming operations:
2453                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2454                            if item["payment"]["currency"] in customStat["payIn"].keys():
2455                                customStat["payIn"][item["payment"]["currency"]] += payment
2456
2457                            else:
2458                                customStat["payIn"][item["payment"]["currency"]] = payment
2459
2460                        # count withdrawals operations:
2461                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2462                            if item["payment"]["currency"] in customStat["payOut"].keys():
2463                                customStat["payOut"][item["payment"]["currency"]] += payment
2464
2465                            else:
2466                                customStat["payOut"][item["payment"]["currency"]] = payment
2467
2468                        # count dividends income:
2469                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2470                            if item["payment"]["currency"] in customStat["divs"].keys():
2471                                customStat["divs"][item["payment"]["currency"]] += payment
2472
2473                            else:
2474                                customStat["divs"][item["payment"]["currency"]] = payment
2475
2476                        # count coupon's income:
2477                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2478                            if item["payment"]["currency"] in customStat["coupons"].keys():
2479                                customStat["coupons"][item["payment"]["currency"]] += payment
2480
2481                            else:
2482                                customStat["coupons"][item["payment"]["currency"]] = payment
2483
2484                        # count broker commissions:
2485                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2486                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2487                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2488
2489                            else:
2490                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2491
2492                        # count service commissions:
2493                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2494                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2495                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2496
2497                            else:
2498                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2499
2500                        # count margin commissions:
2501                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2502                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2503                                customStat["marginCom"][item["payment"]["currency"]] += payment
2504
2505                            else:
2506                                customStat["marginCom"][item["payment"]["currency"]] = payment
2507
2508                        # count withholding taxes:
2509                        elif "_TAX" in item["operationType"]:
2510                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2511                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2512
2513                            else:
2514                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2515
2516                        else:
2517                            continue
2518
2519                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2520
2521                # --- view "Actions" lines:
2522                info.extend([
2523                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2524                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2525                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2526                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2527                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2528                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2529                    ),
2530                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2531                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2532                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2533                    ),
2534                ])
2535
2536                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2537                for key in opsKeys:
2538                    if key == "rub":
2539                        continue
2540
2541                    info.extend([
2542                        "|                            |                               | {:<28} |                      |                        |\n".format(
2543                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2544                        ),
2545                        "|                            |                               | {:<28} |                      |                        |\n".format(
2546                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2547                        ),
2548                    ])
2549
2550                info.append(splitLine1)
2551
2552                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2553                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2554                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2555                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2556                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2557                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2558                    )
2559
2560                # --- view "Payments" lines:
2561                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2562                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2563
2564                for key in paymentsKeys:
2565                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2566
2567                info.append(splitLine1)
2568
2569                # --- view "Commissions and taxes" lines:
2570                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2571                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2572
2573                for key in comKeys:
2574                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2575
2576                info.append(splitLine1)
2577
2578                info.extend([
2579                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2580                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2581                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2582                ])
2583
2584            else:
2585                info.append("Broker returned no operations during this period\n")
2586
2587            # --- view "Operations" section:
2588            for item in ops:
2589                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2590                    continue
2591
2592                else:
2593                    self.figi = item["figi"] if item["figi"] else ""
2594                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2595                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2596
2597                    # group of deals during one day:
2598                    if nextDay and item["date"].split("T")[0] != nextDay:
2599                        info.append(splitLine2)
2600                        nextDay = ""
2601
2602                    else:
2603                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2604
2605                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2606                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2607                        self.figi if self.figi else "—",
2608                        instrument["ticker"] if instrument else "—",
2609                        instrument["type"] if instrument else "—",
2610                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2611                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2612                        TKS_OPERATION_STATES[item["state"]],
2613                        TKS_OPERATION_TYPES[item["operationType"]],
2614                    ))
2615
2616            infoText = "".join(info)
2617
2618            if show:
2619                uLogger.info(infoText)
2620
2621            if self.reportFile:
2622                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2623                    fH.write(infoText)
2624
2625                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2626
2627        return ops, customStat
2628
2629    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2630        """
2631        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2632
2633        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2634        Warning! Broker server used ISO UTC time by default.
2635
2636        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2637        Also, `historyFile` used to update history with `onlyMissing` parameter.
2638
2639        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2640
2641        :param start: see docstring in `GetDatesAsString()` method.
2642        :param end: see docstring in `GetDatesAsString()` method.
2643        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2644                         `"hour"`, `"day"`. Default: `"hour"`.
2645        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2646                            False by default. Warning! History appends only from last candle to current time
2647                            with always update last candle!
2648        :param csvSep: separator if csv-file is used, `,` by default.
2649        :param show: if `True` then also prints Pandas DataFrame to the console.
2650        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2651                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2652        """
2653        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2654        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2655        history = None  # empty pandas object for history
2656
2657        if interval not in TKS_CANDLE_INTERVALS.keys():
2658            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2659            raise Exception("Incorrect value")
2660
2661        if not (self.ticker or self.figi):
2662            uLogger.error("Ticker or FIGI must be defined!")
2663            raise Exception("Ticker or FIGI required")
2664
2665        if self.ticker and not self.figi:
2666            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2667            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2668
2669        if self.figi and not self.ticker:
2670            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2671            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2672
2673        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2674        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2675        if interval.lower() != "day":
2676            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2677
2678        delta = dtEnd - dtStart  # current UTC time minus last time in file
2679        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2680
2681        # calculate history length in candles:
2682        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2683        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2684            length += 1  # to avoid fraction time
2685
2686        # calculate data blocks count:
2687        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2688
2689        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2690        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2691        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2692        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2693        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2694
2695        tempOld = None  # pandas object for old history, if --only-missing key present
2696        lastTime = None  # datetime object of last old candle in file
2697
2698        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2699            uLogger.debug("--only-missing key present, add only last missing candles...")
2700            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2701
2702            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2703
2704            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2705            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2706            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2707            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2708
2709            # get last datetime object from last string in file or minus 1 delta if file is empty:
2710            if len(tempOld) > 0:
2711                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2712
2713            else:
2714                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2715
2716            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2717
2718        responseJSONs = []  # raw history blocks of data
2719
2720        blockEnd = dtEnd
2721        for item in range(blocks):
2722            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2723            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2724
2725            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2726                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2727            ))
2728
2729            if blockStart == blockEnd:
2730                uLogger.debug("Skipped this zero-length block...")
2731
2732            else:
2733                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2734                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2735                self.body = str({
2736                    "figi": self.figi,
2737                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2738                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2739                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2740                })
2741                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2742
2743                if "code" in responseJSON.keys():
2744                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2745
2746                else:
2747                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2748                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2749
2750                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2751
2752            blockEnd = blockStart
2753
2754        printCount = len(responseJSONs)  # candles to show in console
2755        if responseJSONs:
2756            tempHistory = pd.DataFrame(
2757                data={
2758                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2759                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2760                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2761                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2762                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2763                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2764                    "volume": [int(item["volume"]) for item in responseJSONs],
2765                },
2766                index=range(len(responseJSONs)),
2767                columns=["date", "time", "open", "high", "low", "close", "volume"],
2768            )
2769            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2770            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2771
2772            # append only newest candles to old history if --only-missing key present:
2773            if onlyMissing and tempOld is not None and lastTime is not None:
2774                index = 0  # find start index in tempHistory data:
2775
2776                for i, item in tempHistory.iterrows():
2777                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2778
2779                    if curTime == lastTime:
2780                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2781                        index = i
2782                        printCount = index + 1
2783                        break
2784
2785                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2786
2787            else:
2788                history = tempHistory  # if no `--only-missing` key then load full data from server
2789
2790            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2791
2792        if history is not None and not history.empty:
2793            if show:
2794                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2795                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2796                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2797                ))
2798
2799        else:
2800            uLogger.warning("Received an empty candles history!")
2801
2802        if self.historyFile is not None:
2803            if history is not None and not history.empty:
2804                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2805                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2806
2807            else:
2808                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2809
2810        else:
2811            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2812
2813        return history
2814
2815    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2816        """
2817        Load candles history from csv-file and return Pandas DataFrame object.
2818
2819        See also: `History()` and `ShowHistoryChart()` methods.
2820
2821        :param filePath: path to csv-file to open.
2822        """
2823        loadedHistory = None  # init candles data object
2824
2825        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2826
2827        if os.path.exists(filePath):
2828            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2829
2830            tfStr = self.priceModel.FormattedDelta(
2831                self.priceModel.timeframe,
2832                "{days} days {hours}h {minutes}m {seconds}s",
2833            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2834                self.priceModel.timeframe,
2835                "{hours}h {minutes}m {seconds}s",
2836            )
2837
2838            if loadedHistory is not None and not loadedHistory.empty:
2839                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2840                    len(loadedHistory),
2841                    tfStr,
2842                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2843                )
2844
2845            else:
2846                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2847
2848        else:
2849            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2850
2851        return loadedHistory
2852
2853    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2854        """
2855        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2856
2857        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2858        Default: `index.html` (both for interact and non-interact candlesticks chart).
2859
2860        See also: `History()` and `LoadHistory()` methods.
2861
2862        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2863        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2864                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2865                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2866                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2867        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2868                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2869        """
2870        if isinstance(candles, str):
2871            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2872            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2873
2874        elif isinstance(candles, pd.DataFrame):
2875            self.priceModel.prices = candles  # set candles chain from variable
2876            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2877
2878            if "datetime" not in candles.columns:
2879                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2880
2881        else:
2882            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2883            raise Exception("Incorrect value")
2884
2885        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2886
2887        if interact:
2888            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2889
2890            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2891
2892        else:
2893            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2894
2895            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2896
2897        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2898
2899    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2900        """
2901        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2902        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2903
2904        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2905
2906        :param operation: string "Buy" or "Sell".
2907        :param lots: volume, integer count of lots >= 1.
2908        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2909        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2910        :param expDate: string "Undefined" by default or local date in future,
2911                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2912        :return: JSON with response from broker server.
2913        """
2914        if self.accountId is None or not self.accountId:
2915            uLogger.error("Variable `accountId` must be defined for using this method!")
2916            raise Exception("Account ID required")
2917
2918        if operation is None or not operation or operation not in ("Buy", "Sell"):
2919            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2920            raise Exception("Incorrect value")
2921
2922        if lots is None or lots < 1:
2923            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2924            lots = 1
2925
2926        if tp is None or tp < 0:
2927            tp = 0
2928
2929        if sl is None or sl < 0:
2930            sl = 0
2931
2932        if expDate is None or not expDate:
2933            expDate = "Undefined"
2934
2935        if not (self.ticker or self.figi):
2936            uLogger.error("Ticker or FIGI must be defined!")
2937            raise Exception("Ticker or FIGI required")
2938
2939        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2940        self.ticker = instrument["ticker"]
2941        self.figi = instrument["figi"]
2942
2943        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2944
2945        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2946        self.body = str({
2947            "figi": self.figi,
2948            "quantity": str(lots),
2949            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2950            "accountId": str(self.accountId),
2951            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2952        })
2953        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2954
2955        if "orderId" in response.keys():
2956            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2957                operation, response["orderId"],
2958                self.ticker, self.figi, lots,
2959                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2960                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2961                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2962            ))
2963
2964        else:
2965            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2966
2967        if tp > 0:
2968            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2969
2970        if sl > 0:
2971            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2972
2973        return response
2974
2975    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2976        """
2977        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2978        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2979
2980        See also: `Order()` and `Trade()` docstrings.
2981
2982        :param lots: volume, integer count of lots >= 1.
2983        :param tp: float > 0, take profit price of stop-order.
2984        :param sl: float > 0, stop loss price of stop-order.
2985        :param expDate: it's a local date in future.
2986                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2987        :return: JSON with response from broker server.
2988        """
2989        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2990
2991    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2992        """
2993        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2994        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2995
2996        See also: `Order()` and `Trade()` docstrings.
2997
2998        :param lots: volume, integer count of lots >= 1.
2999        :param tp: float > 0, take profit price of stop-order.
3000        :param sl: float > 0, stop loss price of stop-order.
3001        :param expDate: it's a local date in the future.
3002                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3003        :return: JSON with response from broker server.
3004        """
3005        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3006
3007    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
3008        """
3009        Close position of given instruments.
3010
3011        :param tickers: tickers list of instruments that must be closed.
3012        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3013                         This avoids unnecessary downloading data from the server.
3014        """
3015        if not tickers:
3016            uLogger.info("Tickers list is empty, nothing to close.")
3017
3018        else:
3019            if portfolio is None or not portfolio:
3020                portfolio = self.Overview(show=False)
3021
3022            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3023            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
3024
3025            for ticker in tickers:
3026                if ticker not in allOpenedTickers:
3027                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
3028                    continue
3029
3030                # search open trade info about instrument by ticker:
3031                instrument = {}
3032                for iType in TKS_INSTRUMENTS:
3033                    if instrument:
3034                        break
3035
3036                    for item in portfolio["stat"][iType]:
3037                        if item["ticker"] == ticker:
3038                            instrument = item
3039                            break
3040
3041                if instrument:
3042                    self.ticker = ticker
3043                    self.figi = instrument["figi"]
3044
3045                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3046                        self.ticker,
3047                        self.figi,
3048                        int(instrument["volume"]),
3049                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3050                    ))
3051
3052                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3053
3054                    if tradeLots > 0:
3055                        if instrument["blocked"] > 0:
3056                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3057                                instrument["blocked"],
3058                                self.ticker,
3059                                tradeLots,
3060                            ))
3061
3062                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3063                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3064
3065                    else:
3066                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3067
3068    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3069        """
3070        Close all positions of given instruments with defined type.
3071
3072        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3073        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3074                         This avoids unnecessary downloading data from the server.
3075        """
3076        if iType not in TKS_INSTRUMENTS:
3077            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3078
3079        else:
3080            if portfolio is None or not portfolio:
3081                portfolio = self.Overview(show=False)
3082
3083            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3084            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3085
3086            if tickers and portfolio:
3087                self.CloseTrades(tickers, portfolio)
3088
3089            else:
3090                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3091
3092    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3093        """
3094        Universal method to create market or limit orders with all available parameters for current `accountId`.
3095        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3096
3097        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3098        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3099
3100        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3101        then broker immediately open market order as you can do simple --buy or --sell operations!
3102
3103        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3104        When current price will go up or down to target price value then broker opens a limit order.
3105        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3106
3107        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3108
3109        :param operation: string "Buy" or "Sell".
3110        :param orderType: string "Limit" or "Stop".
3111        :param lots: volume, integer count of lots >= 1.
3112        :param targetPrice: target price > 0. This is open trade price for limit order.
3113        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3114                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3115        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3116                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3117                         Stop loss order always executed by market price.
3118        :param expDate: string "Undefined" by default or local date in future.
3119                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3120                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3121                        A limit order has no expiration date, it lasts until the end of the trading day.
3122        :return: JSON with response from broker server.
3123        """
3124        if self.accountId is None or not self.accountId:
3125            uLogger.error("Variable `accountId` must be defined for using this method!")
3126            raise Exception("Account ID required")
3127
3128        if operation is None or not operation or operation not in ("Buy", "Sell"):
3129            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3130            raise Exception("Incorrect value")
3131
3132        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3133            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3134            raise Exception("Incorrect value")
3135
3136        if lots is None or lots < 1:
3137            uLogger.error("You must define trade volume > 0: integer count of lots!")
3138            raise Exception("Incorrect value")
3139
3140        if targetPrice is None or targetPrice <= 0:
3141            uLogger.error("Target price for limit-order must be greater than 0!")
3142            raise Exception("Incorrect value")
3143
3144        if limitPrice is None or limitPrice <= 0:
3145            limitPrice = targetPrice
3146
3147        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3148            stopType = "Limit"
3149
3150        if expDate is None or not expDate:
3151            expDate = "Undefined"
3152
3153        if not (self.ticker or self.figi):
3154            uLogger.error("Tocker or FIGI must be defined!")
3155            raise Exception("Ticker or FIGI required")
3156
3157        response = {}
3158        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3159        self.ticker = instrument["ticker"]
3160        self.figi = instrument["figi"]
3161
3162        if orderType == "Limit":
3163            uLogger.debug(
3164                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3165                    self.ticker, self.figi,
3166                    operation, lots, targetPrice, instrument["currency"],
3167                ))
3168
3169            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3170            self.body = str({
3171                "figi": self.figi,
3172                "quantity": str(lots),
3173                "price": FloatToNano(targetPrice),
3174                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3175                "accountId": str(self.accountId),
3176                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3177            })
3178            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3179
3180            if "orderId" in response.keys():
3181                uLogger.info(
3182                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3183                        response["orderId"],
3184                        self.ticker, self.figi,
3185                        operation, lots, targetPrice, instrument["currency"],
3186                    ))
3187
3188                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3189                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3190                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3191                            targetPrice, instrument["currency"],
3192                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3193                        ))
3194
3195                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3196                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3197                            targetPrice, instrument["currency"],
3198                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3199                        ))
3200
3201            else:
3202                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3203
3204        if orderType == "Stop":
3205            uLogger.debug(
3206                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3207                    self.ticker, self.figi,
3208                    operation, lots,
3209                    targetPrice, instrument["currency"],
3210                    limitPrice, instrument["currency"],
3211                    stopType, expDate,
3212                ))
3213
3214            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3215            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3216            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3217
3218            body = {
3219                "figi": self.figi,
3220                "quantity": str(lots),
3221                "price": FloatToNano(limitPrice),
3222                "stopPrice": FloatToNano(targetPrice),
3223                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3224                "accountId": str(self.accountId),
3225                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3226                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3227            }
3228
3229            if expDateUTC:
3230                body["expireDate"] = expDateUTC
3231
3232            self.body = str(body)
3233            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3234
3235            if "stopOrderId" in response.keys():
3236                uLogger.info(
3237                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3238                        response["stopOrderId"],
3239                        self.ticker, self.figi,
3240                        operation, lots,
3241                        targetPrice, instrument["currency"],
3242                        limitPrice, instrument["currency"],
3243                        TKS_STOP_ORDER_TYPES[stopOrderType],
3244                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3245                    ))
3246
3247                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3248                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3249                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3250                            targetPrice, instrument["currency"],
3251                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3252                        ))
3253
3254                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3255                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3256                            targetPrice, instrument["currency"],
3257                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3258                        ))
3259
3260            else:
3261                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3262
3263        return response
3264
3265    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3266        """
3267        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3268        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3269        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3270        See also: `Order()` docstring.
3271
3272        :param lots: volume, integer count of lots >= 1.
3273        :param targetPrice: target price > 0. This is open trade price for limit order.
3274        :return: JSON with response from broker server.
3275        """
3276        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3277
3278    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3279        """
3280        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3281        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3282        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3283        target price value then broker opens a limit order. See also: `Order()` docstring.
3284
3285        :param lots: volume, integer count of lots >= 1.
3286        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3287        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3288                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3289        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3290                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3291        :param expDate: string "Undefined" by default or local date in future.
3292                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3293                        This date is converting to UTC format for server.
3294        :return: JSON with response from broker server.
3295        """
3296        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3297
3298    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3299        """
3300        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3301        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3302        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3303        See also: `Order()` docstring.
3304
3305        :param lots: volume, integer count of lots >= 1.
3306        :param targetPrice: target price > 0. This is open trade price for limit order.
3307        :return: JSON with response from broker server.
3308        """
3309        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3310
3311    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3312        """
3313        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3314        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3315        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3316        target price value then broker opens a limit order. See also: `Order()` docstring.
3317
3318        :param lots: volume, integer count of lots >= 1.
3319        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3320        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3321                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3322        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3323                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3324        :param expDate: string "Undefined" by default or local date in future.
3325                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3326                        This date is converting to UTC format for server.
3327        :return: JSON with response from broker server.
3328        """
3329        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3330
3331    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3332        """
3333        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3334
3335        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3336        :param allOrdersIDs: pre-received lists of all active pending orders.
3337                             This avoids unnecessary downloading data from the server.
3338        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3339        """
3340        if self.accountId is None or not self.accountId:
3341            uLogger.error("Variable `accountId` must be defined for using this method!")
3342            raise Exception("Account ID required")
3343
3344        if orderIDs:
3345            if allOrdersIDs is None or not allOrdersIDs:
3346                rawOrders = self.RequestPendingOrders()
3347                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3348
3349            if allStopOrdersIDs is None or not allStopOrdersIDs:
3350                rawStopOrders = self.RequestStopOrders()
3351                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3352
3353            for orderID in orderIDs:
3354                idInPendingOrders = orderID in allOrdersIDs
3355                idInStopOrders = orderID in allStopOrdersIDs
3356
3357                if not (idInPendingOrders or idInStopOrders):
3358                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3359                    continue
3360
3361                else:
3362                    if idInPendingOrders:
3363                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3364
3365                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3366                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3367                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3368                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3369
3370                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3371                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3372                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3373
3374                        else:
3375                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3376
3377                    elif idInStopOrders:
3378                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3379
3380                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3381                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3382                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3383                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3384
3385                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3386                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3387                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3388
3389                        else:
3390                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3391
3392                    else:
3393                        continue
3394
3395    def CloseAllOrders(self) -> None:
3396        """
3397        Gets a list of open pending and stop orders and cancel it all.
3398        """
3399        rawOrders = self.RequestPendingOrders()
3400        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3401        lenOrders = len(allOrdersIDs)
3402
3403        rawStopOrders = self.RequestStopOrders()
3404        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3405        lenSOrders = len(allStopOrdersIDs)
3406
3407        if lenOrders > 0 or lenSOrders > 0:
3408            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3409
3410            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3411
3412        else:
3413            uLogger.info("Orders not found, nothing to cancel.")
3414
3415    def CloseAll(self, *args) -> None:
3416        """
3417        Close all available (not blocked) opened trades and orders.
3418
3419        Also, you can select one or more keywords case-insensitive:
3420        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3421
3422        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3423        """
3424        overview = self.Overview(show=False)  # get all open trades info
3425
3426        if len(args) == 0:
3427            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3428            self.CloseAllOrders()  # close all pending and stop orders
3429
3430            for iType in TKS_INSTRUMENTS:
3431                if iType != "Currencies":
3432                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3433
3434        else:
3435            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3436            lowerArgs = [x.lower() for x in args]
3437
3438            if "orders" in lowerArgs:
3439                self.CloseAllOrders()  # close all pending and stop orders
3440
3441            for iType in TKS_INSTRUMENTS:
3442                if iType.lower() in lowerArgs and iType != "Currencies":
3443                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3444
3445    @staticmethod
3446    def ParseOrderParameters(operation, **inputParameters):
3447        """
3448        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3449
3450        :param operation: string "Buy" or "Sell".
3451        :param inputParameters: this is dict of strings that looks like this
3452               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3453               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3454               "prices" key: one or more prices to open limit-orders
3455               Counts of values in lots and prices lists must be equals!
3456        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3457        """
3458        # TODO: update order grid work with api v2
3459        pass
3460        # uLogger.debug("Input parameters: {}".format(inputParameters))
3461        #
3462        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3463        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3464        #     raise Exception("Incorrect value")
3465        #
3466        # if "l" in inputParameters.keys():
3467        #     inputParameters["lots"] = inputParameters.pop("l")
3468        #
3469        # if "p" in inputParameters.keys():
3470        #     inputParameters["prices"] = inputParameters.pop("p")
3471        #
3472        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3473        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3474        #     raise Exception("Incorrect value")
3475        #
3476        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3477        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3478        #
3479        # if len(lots) != len(prices):
3480        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3481        #     raise Exception("Incorrect value")
3482        #
3483        # uLogger.debug("Extracted parameters for orders:")
3484        # uLogger.debug("lots = {}".format(lots))
3485        # uLogger.debug("prices = {}".format(prices))
3486        #
3487        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3488        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3489        # uLogger.debug("Order parameters: {}".format(result))
3490        #
3491        # return result
3492
3493    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3494        """
3495        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3496
3497        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3498        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3499        """
3500        result = False
3501        msg = "Instrument not defined!"
3502
3503        if portfolio is None or not portfolio:
3504            portfolio = self.Overview(show=False)
3505
3506        if self.ticker:
3507            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3508            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3509
3510            for iType in TKS_INSTRUMENTS:
3511                for instrument in portfolio["stat"][iType]:
3512                    if instrument["ticker"] == self.ticker:
3513                        result = True
3514                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3515                        break
3516
3517        elif self.figi:
3518            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3519            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3520
3521            for iType in TKS_INSTRUMENTS:
3522                for instrument in portfolio["stat"][iType]:
3523                    if instrument["figi"] == self.figi:
3524                        result = True
3525                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3526                        break
3527
3528        else:
3529            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3530
3531        uLogger.debug(msg)
3532
3533        return result
3534
3535    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3536        """
3537        Returns instrument is in the user's portfolio if it presents there.
3538        Instrument must be defined by `ticker` (highly priority) or `figi`.
3539
3540        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3541        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3542        """
3543        result = None
3544        msg = "Instrument not defined!"
3545
3546        if portfolio is None or not portfolio:
3547            portfolio = self.Overview(show=False)
3548
3549        if self.ticker:
3550            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3551            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3552
3553            for iType in TKS_INSTRUMENTS:
3554                for instrument in portfolio["stat"][iType]:
3555                    if instrument["ticker"] == self.ticker:
3556                        result = instrument
3557                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3558                        break
3559
3560        elif self.figi:
3561            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3562            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3563
3564            for iType in TKS_INSTRUMENTS:
3565                for instrument in portfolio["stat"][iType]:
3566                    if instrument["figi"] == self.figi:
3567                        result = instrument
3568                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3569                        break
3570
3571        else:
3572            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3573
3574        uLogger.debug(msg)
3575
3576        return result
3577
3578    def RequestLimits(self) -> dict:
3579        """
3580        Method for obtaining the available funds for withdrawal for current `accountId`.
3581
3582        See also:
3583        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3584        - `OverviewLimits()` method
3585
3586        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3587                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3588                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3589                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3590        """
3591        if self.accountId is None or not self.accountId:
3592            uLogger.error("Variable `accountId` must be defined for using this method!")
3593            raise Exception("Account ID required")
3594
3595        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3596
3597        self.body = str({"accountId": self.accountId})
3598        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3599        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3600
3601        uLogger.debug("Records about available funds for withdrawal successfully received")
3602
3603        return rawLimits
3604
3605    def OverviewLimits(self, show: bool = False) -> dict:
3606        """
3607        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3608
3609        See also: `RequestLimits()`.
3610
3611        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3612        :return: dict with raw parsed data from server and some calculated statistics about it.
3613        """
3614        if self.accountId is None or not self.accountId:
3615            uLogger.error("Variable `accountId` must be defined for using this method!")
3616            raise Exception("Account ID required")
3617
3618        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3619
3620        view = {
3621            "rawLimits": rawLimits,
3622            "limits": {  # parsed data for every currency:
3623                "money": {  # this is an array of portfolio currency positions
3624                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3625                },
3626                "blocked": {  # this is an array of blocked currency
3627                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3628                },
3629                "blockedGuarantee": {  # this is locked money under collateral for futures
3630                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3631                },
3632            },
3633        }
3634
3635        # --- Prepare text table with limits in human-readable format:
3636        if show:
3637            info = [
3638                "# Withdrawal limits\n\n",
3639                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3640                "* **Account ID:** [{}]\n".format(self.accountId),
3641                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3642                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3643            ]
3644
3645            for curr in view["limits"]["money"].keys():
3646                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3647                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3648                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3649
3650                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3651                    "[{}]".format(curr),
3652                    "{:.2f}".format(view["limits"]["money"][curr]),
3653                    "{:.2f}".format(availableMoney),
3654                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3655                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3656                )
3657
3658                if curr == "rub":
3659                    info.insert(5, infoStr)  # insert at first position in table and after headers
3660
3661                else:
3662                    info.append(infoStr)
3663
3664            infoText = "".join(info)
3665
3666            uLogger.info(infoText)
3667
3668            if self.withdrawalLimitsFile:
3669                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3670                    fH.write(infoText)
3671
3672                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3673
3674        return view
3675
3676    def RequestAccounts(self) -> dict:
3677        """
3678        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3679
3680        See also:
3681        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3682        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3683        - `OverviewUserInfo()` method
3684
3685        :return: dict with raw data from server that contains accounts info. Example of dict:
3686                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3687                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3688                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3689                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3690        """
3691        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3692
3693        self.body = str({})
3694        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3695        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3696
3697        uLogger.debug("Records about available accounts successfully received")
3698
3699        return rawAccounts
3700
3701    def RequestUserInfo(self) -> dict:
3702        """
3703        Method for requesting common user's information.
3704
3705        See also:
3706        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3707        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3708        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3709        - `OverviewUserInfo()` method
3710
3711        :return: dict with raw data from server that contains user's information. Example of dict:
3712                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3713                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3714        """
3715        uLogger.debug("Requesting common user's information. Wait, please...")
3716
3717        self.body = str({})
3718        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3719        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3720
3721        uLogger.debug("Records about current user successfully received")
3722
3723        return rawUserInfo
3724
3725    def RequestMarginStatus(self, accountId: str = None) -> dict:
3726        """
3727        Method for requesting margin calculation for defined account ID.
3728
3729        See also:
3730        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3731        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3732        - `OverviewUserInfo()` method
3733
3734        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3735        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3736                 Example of responses:
3737                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3738                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3739                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3740                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3741                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3742                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3743        """
3744        if accountId is None or not accountId:
3745            if self.accountId is None or not self.accountId:
3746                uLogger.error("Variable `accountId` must be defined for using this method!")
3747                raise Exception("Account ID required")
3748
3749            else:
3750                accountId = self.accountId  # use `self.accountId` (main ID) by default
3751
3752        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3753
3754        self.body = str({"accountId": accountId})
3755        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3756        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3757
3758        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3759            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3760            rawMargin = {}
3761
3762        else:
3763            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3764
3765        return rawMargin
3766
3767    def RequestTariffLimits(self) -> dict:
3768        """
3769        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3770
3771        See also:
3772        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3773        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3774        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3775        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3776        - `OverviewUserInfo()` method
3777
3778        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3779                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3780                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3781        """
3782        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3783
3784        self.body = str({})
3785        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3786        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3787
3788        uLogger.debug("Records with limits of current tariff successfully received")
3789
3790        return rawTariffLimits
3791
3792    def RequestBondCoupons(self, iJSON: dict) -> dict:
3793        """
3794        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3795        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3796        All dates are in UTC timezone.
3797
3798        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3799        Documentation:
3800        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3801        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3802
3803        See also: `ExtendBondsData()`.
3804
3805        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3806                      If raw iJSON is not data of bond then server returns an error [400] with message:
3807                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3808        :return: dictionary with bond payment calendar. Response example
3809                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3810                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3811                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3812                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3813        """
3814        if iJSON["figi"] is None or not iJSON["figi"]:
3815            uLogger.error("FIGI must be defined for using this method!")
3816            raise Exception("FIGI required")
3817
3818        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3819        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3820
3821        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3822            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3823            self.figi,
3824            startDate,
3825            endDate,
3826        ))
3827
3828        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3829        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3830        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3831
3832        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3833            uLogger.warning("Instrument type is not bond!")
3834
3835        else:
3836            uLogger.debug("Records about bond payment calendar successfully received")
3837
3838        return calendar
3839
3840    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3841        """
3842        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3843        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3844        coupon yields, current yields and some statistics etc.
3845
3846        WARNING! This is too long operation if a lot of bonds requested from broker server.
3847
3848        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3849
3850        :param instruments: list of strings with tickers or FIGIs.
3851        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3852                     for further used by data scientists or stock analytics.
3853        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3854                 In XLSX-file and Pandas DataFrame fields mean:
3855                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3856                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3857        """
3858        if instruments is None or not instruments:
3859            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3860            raise Exception("Ticker or FIGI required")
3861
3862        if isinstance(instruments, str):
3863            instruments = [instruments]
3864
3865        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3866
3867        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3868
3869        iCount = len(uniqueInstruments)
3870        tooLong = iCount >= 20
3871        if tooLong:
3872            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3873
3874        bonds = None
3875        for i, self.figi in enumerate(uniqueInstruments):
3876            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3877
3878            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3879                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3880                rawBond = self.SearchByFIGI(requestPrice=True)
3881
3882                # Widen raw data with UTC current time (iData["actualDateTime"]):
3883                actualDate = datetime.now(tzutc())
3884                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3885
3886                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3887                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3888
3889                # Replace some values with human-readable:
3890                iData["nominalCurrency"] = iData["nominal"]["currency"]
3891                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3892                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3893                iData["aciCurrency"] = iData["aciValue"]["currency"]
3894                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3895                iData["issueSize"] = int(iData["issueSize"])
3896                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3897                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3898                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3899                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3900                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3901                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3902                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3903                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3904                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3905                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3906
3907                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3908                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3909                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3910                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3911                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3912                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3913                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3914                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3915                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3916                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3917                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3918
3919                # Widen raw data with calendar data from `rawCalendar` values:
3920                calendarData = []
3921                for item in iData["rawCalendar"]["events"]:
3922                    calendarData.append({
3923                        "couponDate": item["couponDate"],
3924                        "couponNumber": int(item["couponNumber"]),
3925                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3926                        "payCurrency": item["payOneBond"]["currency"],
3927                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3928                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3929                        "couponStartDate": item["couponStartDate"],
3930                        "couponEndDate": item["couponEndDate"],
3931                        "couponPeriod": item["couponPeriod"],
3932                    })
3933
3934                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3935                if "maturityDate" not in iData.keys():
3936                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3937
3938                # Widen raw data with Coupon Rate.
3939                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3940                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3941                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3942                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3943
3944                # Widen raw data with Yield to Maturity (YTM) on current date.
3945                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3946                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3947                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3948                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3949                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3950                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3951
3952                iData["calendar"] = calendarData  # adds calendar at the end
3953
3954                # Remove not used data:
3955                iData.pop("uid")
3956                iData.pop("positionUid")
3957                iData.pop("currentPrice")
3958                iData.pop("rawCalendar")
3959
3960                colNames = list(iData.keys())
3961                if bonds is None:
3962                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3963
3964                else:
3965                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3966
3967            else:
3968                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3969
3970            processed = round(100 * (i + 1) / iCount, 1)
3971            if tooLong and processed % 5 == 0:
3972                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3973
3974            else:
3975                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3976
3977        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3978
3979        # Saving bonds from Pandas DataFrame to XLSX sheet:
3980        if xlsx and self.bondsXLSXFile:
3981            with pd.ExcelWriter(
3982                    path=self.bondsXLSXFile,
3983                    date_format=TKS_DATE_FORMAT,
3984                    datetime_format=TKS_DATE_TIME_FORMAT,
3985                    mode="w",
3986            ) as writer:
3987                bonds.to_excel(
3988                    writer,
3989                    sheet_name="Extended bonds data",
3990                    index=True,
3991                    encoding="UTF-8",
3992                    freeze_panes=(1, 1),
3993                )  # saving as XLSX-file with freeze first row and column as headers
3994
3995            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3996
3997        return bonds
3998
3999    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4000        """
4001        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4002
4003        WARNING! This is too long operation if a lot of bonds requested from broker server.
4004
4005        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4006
4007        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4008                        extended information about bonds: main info, current prices, bond payment calendar,
4009                        coupon yields, current yields and some statistics etc.
4010                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4011        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4012                     for further used by data scientists or stock analytics.
4013        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4014        """
4015        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4016            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4017
4018        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4019
4020        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4021        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4022        calendar = None
4023        for bond in extBonds.iterrows():
4024            for item in bond[1]["calendar"]:
4025                cData = {
4026                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4027                    "couponDate": item["couponDate"],
4028                    "figi": bond[1]["figi"],
4029                    "ticker": bond[1]["ticker"],
4030                    "name": bond[1]["name"],
4031                    "couponNumber": item["couponNumber"],
4032                    "payOneBond": item["payOneBond"],
4033                    "payCurrency": item["payCurrency"],
4034                    "couponType": item["couponType"],
4035                    "couponPeriod": item["couponPeriod"],
4036                    "fixDate": item["fixDate"],
4037                    "couponStartDate": item["couponStartDate"],
4038                    "couponEndDate": item["couponEndDate"],
4039                }
4040
4041                if calendar is None:
4042                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4043
4044                else:
4045                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4046
4047        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4048
4049        # Saving calendar from Pandas DataFrame to XLSX sheet:
4050        if xlsx:
4051            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4052
4053            with pd.ExcelWriter(
4054                    path=xlsxCalendarFile,
4055                    date_format=TKS_DATE_FORMAT,
4056                    datetime_format=TKS_DATE_TIME_FORMAT,
4057                    mode="w",
4058            ) as writer:
4059                humanReadable = calendar.copy(deep=True)
4060                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4061                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4062                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4063                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4064                humanReadable.columns = colNames  # human-readable column names
4065
4066                humanReadable.to_excel(
4067                    writer,
4068                    sheet_name="Bond payments calendar",
4069                    index=False,
4070                    encoding="UTF-8",
4071                    freeze_panes=(1, 2),
4072                )  # saving as XLSX-file with freeze first row and column as headers
4073
4074                del humanReadable  # release df in memory
4075
4076            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4077
4078        return calendar
4079
4080    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4081        """
4082        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4083        Also, creates Markdown file with calendar data, `calendar.md` by default.
4084
4085        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4086
4087        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4088                        extended information about bonds: main info, current prices, bond payment calendar,
4089                        coupon yields, current yields and some statistics etc.
4090                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4091        :param show: if `True` then also printing bonds payment calendar to the console,
4092                     otherwise save to file `calendarFile` only. `False` by default.
4093        :return: multilines text in Markdown format with bonds payment calendar as a table.
4094        """
4095        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4096            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4097
4098        infoText = "# Bond payments calendar\n\n"
4099
4100        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4101
4102        if not calendar.empty:
4103            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4104
4105            info = [
4106                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4107                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4108            ]
4109
4110            newMonth = False
4111            notOneBond = calendar["figi"].nunique() > 1
4112            for i, bond in enumerate(calendar.iterrows()):
4113                if newMonth and notOneBond:
4114                    info.append(splitLine)
4115
4116                info.append(
4117                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4118                        "  √" if bond[1]["paid"] else "  —",
4119                        bond[1]["couponDate"].split("T")[0],
4120                        bond[1]["figi"],
4121                        bond[1]["ticker"],
4122                        bond[1]["couponNumber"],
4123                        "{} {}".format(
4124                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4125                            bond[1]["payCurrency"],
4126                        ),
4127                        bond[1]["couponType"],
4128                        bond[1]["couponPeriod"],
4129                        bond[1]["fixDate"].split("T")[0],
4130                    )
4131                )
4132
4133                if i < len(calendar.values) - 1:
4134                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4135                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4136                    newMonth = False if curDate.month == nextDate.month else True
4137
4138                else:
4139                    newMonth = False
4140
4141            infoText += "".join(info)
4142
4143            if show:
4144                uLogger.info("{}".format(infoText))
4145
4146            if self.calendarFile is not None:
4147                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4148                    fH.write(infoText)
4149
4150                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4151
4152        else:
4153            infoText += "No data\n"
4154
4155        return infoText
4156
4157    def OverviewAccounts(self, show: bool = False) -> dict:
4158        """
4159        Method for parsing and show simple table with all available user accounts.
4160
4161        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4162
4163        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4164        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4165                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4166                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4167                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4168                                                        "closed": "—", "access": "Full access" }, ...}}`
4169        """
4170        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4171
4172        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4173        accounts = {
4174            item["id"]: {
4175                "type": TKS_ACCOUNT_TYPES[item["type"]],
4176                "name": item["name"],
4177                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4178                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4179                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4180                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4181            } for item in rawAccounts["accounts"]
4182        }
4183
4184        # Raw and parsed data with some fields replaced in "stat" section:
4185        view = {
4186            "rawAccounts": rawAccounts,
4187            "stat": accounts,
4188        }
4189
4190        # --- Prepare simple text table with only accounts data in human-readable format:
4191        if show:
4192            info = [
4193                "# User accounts\n\n",
4194                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4195                "| Account ID   | Type                      | Status                    | Name                           |\n",
4196                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4197            ]
4198
4199            for account in view["stat"].keys():
4200                info.extend([
4201                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4202                        account,
4203                        view["stat"][account]["type"],
4204                        view["stat"][account]["status"],
4205                        view["stat"][account]["name"],
4206                    )
4207                ])
4208
4209            infoText = "".join(info)
4210
4211            uLogger.info(infoText)
4212
4213            if self.userAccountsFile:
4214                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4215                    fH.write(infoText)
4216
4217                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4218
4219        return view
4220
4221    def OverviewUserInfo(self, show: bool = False) -> dict:
4222        """
4223        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4224
4225        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4226
4227        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4228        :return: dict with raw parsed data from server and some calculated statistics about it.
4229        """
4230        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4231        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4232        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4233        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4234        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4235        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4236
4237        # This is dict with parsed common user data:
4238        userInfo = {
4239            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4240            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4241            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4242            "tariff": rawUserInfo["tariff"],
4243        }
4244
4245        # This is an array of dict with parsed margin statuses for every account IDs:
4246        margins = {}
4247        for accountId in accounts.keys():
4248            if rawMargins[accountId]:
4249                margins[accountId] = {
4250                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4251                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4252                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4253                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4254                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4255                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4256                }
4257
4258            else:
4259                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4260
4261        unary = {}  # unary-connection limits
4262        for item in rawTariffLimits["unaryLimits"]:
4263            if item["limitPerMinute"] in unary.keys():
4264                unary[item["limitPerMinute"]].extend(item["methods"])
4265
4266            else:
4267                unary[item["limitPerMinute"]] = item["methods"]
4268
4269        stream = {}  # stream-connection limits
4270        for item in rawTariffLimits["streamLimits"]:
4271            if item["limit"] in stream.keys():
4272                stream[item["limit"]].extend(item["streams"])
4273
4274            else:
4275                stream[item["limit"]] = item["streams"]
4276
4277        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4278        limits = {
4279            "unary": unary,
4280            "stream": stream,
4281        }
4282
4283        # Raw and parsed data as an output result:
4284        view = {
4285            "rawUserInfo": rawUserInfo,
4286            "rawAccounts": rawAccounts,
4287            "rawMargins": rawMargins,
4288            "rawTariffLimits": rawTariffLimits,
4289            "stat": {
4290                "userInfo": userInfo,
4291                "accounts": accounts,
4292                "margins": margins,
4293                "limits": limits,
4294            },
4295        }
4296
4297        # --- Prepare text table with user information in human-readable format:
4298        if show:
4299            info = [
4300                "# Full user information\n\n",
4301                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4302                "## Common information\n\n",
4303                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4304                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4305                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4306                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4307                "\n## User accounts\n\n",
4308            ]
4309
4310            for account in view["stat"]["accounts"].keys():
4311                info.extend([
4312                    "### ID: [{}]\n\n".format(account),
4313                    "| Parameters           | Values                                                       |\n",
4314                    "|----------------------|--------------------------------------------------------------|\n",
4315                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4316                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4317                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4318                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4319                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4320                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4321                ])
4322
4323                if margins[account]:
4324                    info.extend([
4325                        "| Margin status:       | Enabled                                                      |\n",
4326                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4327                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4328                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4329                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4330                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4331                    ])
4332
4333                else:
4334                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4335
4336            info.extend([
4337                "\n## Current user tariff limits\n",
4338                "\nSee also:\n",
4339                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4340                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4341                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4342                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4343                "\n### Unary limits\n",
4344            ])
4345
4346            if unary:
4347                for key, values in sorted(unary.items()):
4348                    info.append("\n* Max requests per minute: {}\n".format(key))
4349
4350                    for value in values:
4351                        info.append("  - {}\n".format(value))
4352
4353            else:
4354                info.append("\nNot available\n")
4355
4356            info.append("\n### Stream limits\n")
4357
4358            if stream:
4359                for key, values in sorted(stream.items()):
4360                    info.append("\n* Max stream connections: {}\n".format(key))
4361
4362                    for value in values:
4363                        info.append("  - {}\n".format(value))
4364
4365            else:
4366                info.append("\nNot available\n")
4367
4368            infoText = "".join(info)
4369
4370            uLogger.info(infoText)
4371
4372            if self.userInfoFile:
4373                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4374                    fH.write(infoText)
4375
4376                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4377
4378        return view
4379
4380
4381class Args:
4382    """
4383    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4384    """
4385    def __init__(self, **kwargs):
4386        self.__dict__.update(kwargs)
4387
4388    def __getattr__(self, item):
4389        return None
4390
4391
4392def ParseArgs():
4393    """This function get and parse command line keys."""
4394    parser = ArgumentParser()  # command-line string parser
4395
4396    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4397    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4398
4399    # --- options:
4400
4401    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4402    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4403    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4404
4405    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4406    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4407
4408    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4409    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4410
4411    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4412
4413    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4414    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4415    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4416
4417    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4418
4419    # --- commands:
4420
4421    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4422
4423    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4424    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4425    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4426    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4427    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4428    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4429    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4430    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4431
4432    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4433    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4434    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4435    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4436    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4437
4438    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4439    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4440    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4441    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4442
4443    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4444    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4445    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4446
4447    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4448    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4449    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4450    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4451    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4452    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4453    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4454
4455    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4456    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4457    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4458    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4459    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4460
4461    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4462    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4463    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4464
4465    cmdArgs = parser.parse_args()
4466    return cmdArgs
4467
4468
4469def Main(**kwargs):
4470    """
4471    Main function for work with TKSBrokerAPI in the console.
4472
4473    See examples:
4474    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4475    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4476    """
4477    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4478
4479    if args.debug_level:
4480        uLogger.level = 10  # always debug level by default
4481        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4482
4483    exitCode = 0
4484    start = datetime.now(tzutc())
4485    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4486        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4487        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4488    ))
4489
4490    # trying to calculate full current version:
4491    buildVersion = __version__
4492    try:
4493        v = version("tksbrokerapi")
4494        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4495
4496    except Exception:
4497        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4498
4499    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4500    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4501
4502    try:
4503        if args.version:
4504            print("TKSBrokerAPI {}".format(buildVersion))
4505            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4506
4507        else:
4508            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4509            server = TinkoffBrokerServer(
4510                token=args.token,
4511                accountId=args.account_id,
4512                useCache=not args.no_cache,
4513            )
4514
4515            # --- set some options:
4516
4517            if args.ticker:
4518                if args.ticker in server.aliasesKeys:
4519                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4520
4521                else:
4522                    server.ticker = args.ticker
4523
4524            if args.figi:
4525                server.figi = args.figi
4526
4527            if args.depth is not None:
4528                server.depth = args.depth
4529
4530            # --- do one of commands:
4531
4532            if args.list:
4533                if args.output is not None:
4534                    server.instrumentsFile = args.output
4535
4536                server.ShowInstrumentsInfo(show=True)
4537
4538            elif args.list_xlsx:
4539                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4540
4541            elif args.bonds_xlsx is not None:
4542                if args.output is not None:
4543                    server.bondsXLSXFile = args.output
4544
4545                if len(args.bonds_xlsx) == 0:
4546                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4547
4548                else:
4549                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4550
4551            elif args.search:
4552                if args.output is not None:
4553                    server.searchResultsFile = args.output
4554
4555                server.SearchInstruments(pattern=args.search[0], show=True)
4556
4557            elif args.info:
4558                if not (args.ticker or args.figi):
4559                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4560                    raise Exception("Ticker or FIGI required")
4561
4562                if args.output is not None:
4563                    server.infoFile = args.output
4564
4565                if args.ticker:
4566                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4567
4568                else:
4569                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4570
4571            elif args.calendar is not None:
4572                if args.output is not None:
4573                    server.calendarFile = args.output
4574
4575                if len(args.calendar) == 0:
4576                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4577
4578                else:
4579                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4580
4581                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4582
4583            elif args.price:
4584                if not (args.ticker or args.figi):
4585                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4586                    raise Exception("Ticker or FIGI required")
4587
4588                server.GetCurrentPrices(show=True)
4589
4590            elif args.prices is not None:
4591                if args.output is not None:
4592                    server.pricesFile = args.output
4593
4594                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4595
4596            elif args.overview:
4597                if args.output is not None:
4598                    server.overviewFile = args.output
4599
4600                server.Overview(show=True, details="full")
4601
4602            elif args.overview_digest:
4603                if args.output is not None:
4604                    server.overviewDigestFile = args.output
4605
4606                server.Overview(show=True, details="digest")
4607
4608            elif args.overview_positions:
4609                if args.output is not None:
4610                    server.overviewPositionsFile = args.output
4611
4612                server.Overview(show=True, details="positions")
4613
4614            elif args.overview_orders:
4615                if args.output is not None:
4616                    server.overviewOrdersFile = args.output
4617
4618                server.Overview(show=True, details="orders")
4619
4620            elif args.overview_analytics:
4621                if args.output is not None:
4622                    server.overviewAnalyticsFile = args.output
4623
4624                server.Overview(show=True, details="analytics")
4625
4626            elif args.deals is not None:
4627                if args.output is not None:
4628                    server.reportFile = args.output
4629
4630                if 0 <= len(args.deals) < 3:
4631                    server.Deals(
4632                        start=args.deals[0] if len(args.deals) >= 1 else None,
4633                        end=args.deals[1] if len(args.deals) == 2 else None,
4634                        show=True,  # Always show deals report in console
4635                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4636                    )
4637
4638                else:
4639                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4640                    raise Exception("Incorrect value")
4641
4642            elif args.history is not None:
4643                if args.output is not None:
4644                    server.historyFile = args.output
4645
4646                if 0 <= len(args.history) < 3:
4647                    dataReceived = server.History(
4648                        start=args.history[0] if len(args.history) >= 1 else None,
4649                        end=args.history[1] if len(args.history) == 2 else None,
4650                        interval="hour" if args.interval is None or not args.interval else args.interval,
4651                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4652                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4653                        show=True,  # shows all downloaded candles in console
4654                    )
4655
4656                    if args.render_chart is not None and dataReceived is not None:
4657                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4658
4659                        server.ShowHistoryChart(
4660                            candles=dataReceived,
4661                            interact=iChart,
4662                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4663                        )
4664
4665                else:
4666                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4667                    raise Exception("Incorrect value")
4668
4669            elif args.load_history is not None:
4670                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4671
4672                if args.render_chart is not None and histData is not None:
4673                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4674                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4675
4676                    server.ShowHistoryChart(
4677                        candles=histData,
4678                        interact=iChart,
4679                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4680                    )
4681
4682            elif args.trade is not None:
4683                if 1 <= len(args.trade) <= 5:
4684                    server.Trade(
4685                        operation=args.trade[0],
4686                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4687                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4688                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4689                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4690                    )
4691
4692                else:
4693                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4694
4695            elif args.buy is not None:
4696                if 0 <= len(args.buy) <= 4:
4697                    server.Buy(
4698                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4699                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4700                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4701                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4702                    )
4703
4704                else:
4705                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4706
4707            elif args.sell is not None:
4708                if 0 <= len(args.sell) <= 4:
4709                    server.Sell(
4710                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4711                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4712                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4713                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4714                    )
4715
4716                else:
4717                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4718
4719            elif args.order:
4720                if 4 <= len(args.order) <= 7:
4721                    server.Order(
4722                        operation=args.order[0],
4723                        orderType=args.order[1],
4724                        lots=int(args.order[2]),
4725                        targetPrice=float(args.order[3]),
4726                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4727                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4728                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4729                    )
4730
4731                else:
4732                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4733
4734            elif args.buy_limit:
4735                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4736
4737            elif args.sell_limit:
4738                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4739
4740            elif args.buy_stop:
4741                if 2 <= len(args.buy_stop) <= 7:
4742                    server.BuyStop(
4743                        lots=int(args.buy_stop[0]),
4744                        targetPrice=float(args.buy_stop[1]),
4745                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4746                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4747                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4748                    )
4749
4750                else:
4751                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4752
4753            elif args.sell_stop:
4754                if 2 <= len(args.sell_stop) <= 7:
4755                    server.SellStop(
4756                        lots=int(args.sell_stop[0]),
4757                        targetPrice=float(args.sell_stop[1]),
4758                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4759                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4760                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4761                    )
4762
4763                else:
4764                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4765
4766            # elif args.buy_order_grid is not None:
4767            #     # update order grid work with api v2
4768            #     if len(args.buy_order_grid) == 2:
4769            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4770            #
4771            #         for order in orderParams:
4772            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4773            #
4774            #     else:
4775            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4776            #
4777            # elif args.sell_order_grid is not None:
4778            #     # update order grid work with api v2
4779            #     if len(args.sell_order_grid) >= 2:
4780            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4781            #
4782            #         for order in orderParams:
4783            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4784            #
4785            #     else:
4786            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4787
4788            elif args.close_order is not None:
4789                server.CloseOrders(args.close_order)  # close only one order
4790
4791            elif args.close_orders is not None:
4792                server.CloseOrders(args.close_orders)  # close list of orders
4793
4794            elif args.close_trade:
4795                if not args.ticker:
4796                    uLogger.error("`--ticker` key is required for this operation!")
4797                    raise Exception("Ticker required")
4798
4799                server.CloseTrades([args.ticker])  # close only one trade
4800
4801            elif args.close_trades is not None:
4802                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4803
4804            elif args.close_all is not None:
4805                server.CloseAll(*args.close_all)
4806
4807            elif args.limits:
4808                if args.output is not None:
4809                    server.withdrawalLimitsFile = args.output
4810
4811                server.OverviewLimits(show=True)
4812
4813            elif args.user_info:
4814                if args.output is not None:
4815                    server.userInfoFile = args.output
4816
4817                server.OverviewUserInfo(show=True)
4818
4819            elif args.account:
4820                if args.output is not None:
4821                    server.userAccountsFile = args.output
4822
4823                server.OverviewAccounts(show=True)
4824
4825            else:
4826                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4827                raise Exception("There is no command to execute")
4828
4829    except Exception:
4830        trace = tb.format_exc()
4831        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4832            if e in trace:
4833                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4834                break
4835
4836        uLogger.debug(trace)
4837        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4838        exitCode = 255  # an error occurred, must be open a ticket for this issue
4839
4840    finally:
4841        finish = datetime.now(tzutc())
4842
4843        if exitCode == 0:
4844            uLogger.debug("All operations were finished success (summary code is 0).")
4845
4846        else:
4847            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4848                os.path.abspath(uLog.defaultLogFile), exitCode,
4849            ))
4850
4851        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4852        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4853            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4854            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4855        ))
4856
4857        if not kwargs:
4858            sys.exit(exitCode)
4859
4860        else:
4861            return exitCode
4862
4863
4864if __name__ == "__main__":
4865    Main()
def NanoToFloat(units: str, nano: int) -> float:
80def NanoToFloat(units: str, nano: int) -> float:
81    """
82    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
83
84    `NanoToFloat(units="2", nano=500000000) -> 2.5`
85
86    `NanoToFloat(units="0", nano=50000000) -> 0.05`
87
88    :param units: integer string or integer parameter that represents the integer part of number
89    :param nano: integer string or integer parameter that represents the fractional part of number
90    :return: float view of number
91    """
92    return int(units) + int(nano) * NANO

Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:

NanoToFloat(units="2", nano=500000000) -> 2.5

NanoToFloat(units="0", nano=50000000) -> 0.05

Parameters
  • units: integer string or integer parameter that represents the integer part of number
  • nano: integer string or integer parameter that represents the fractional part of number
Returns

float view of number

def FloatToNano(number: float) -> dict:
 95def FloatToNano(number: float) -> dict:
 96    """
 97    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
 98
 99    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
100
101    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
102
103    :param number: float number
104    :return: nano-type view of number: `{"units": "string", "nano": integer}`
105    """
106    splitByPoint = str(number).split(".")
107    frac = 0
108
109    if len(splitByPoint) > 1:
110        if len(splitByPoint[1]) <= 9:
111            frac = int("{}{}".format(
112                int(splitByPoint[1]),
113                "0" * (9 - len(splitByPoint[1])),
114            ))
115
116    if (number < 0) and (frac > 0):
117        frac = -frac
118
119    return {"units": str(int(number)), "nano": frac}

Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:

FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}

FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}

Parameters
  • number: float number
Returns

nano-type view of number: {"units": "string", "nano": integer}

def GetDatesAsString(start: str = None, end: str = None) -> tuple:
122def GetDatesAsString(start: str = None, end: str = None) -> tuple:
123    """
124    Create tuple of date and time strings with timezone parsed from user-friendly date.
125
126    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
127
128    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
129    An error exception will occur if input date has incorrect format.
130
131    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
132    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
133    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
134    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
135
136    Also, you can use keywords for start if `end=None`:
137    `today` (from 00:00:00 to the end of current day),
138    `yesterday` (-1 day from 00:00:00 to 23:59:59),
139    `week` (-7 day from 00:00:00 to the end of current day),
140    `month` (-30 day from 00:00:00 to the end of current day),
141    `year` (-365 day from 00:00:00 to the end of current day),
142
143    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
144             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
145             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
146    """
147    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
148    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
149    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
150
151    # time between start and the end of the current day:
152    if start is None or start.lower() == "today":
153        pass
154
155    # from start of the last day to the end of the last day:
156    elif start.lower() == "yesterday":
157        s -= timedelta(days=1)
158        e -= timedelta(days=1)
159
160    # week (-7 day from 00:00:00 to the end of the current day):
161    elif start.lower() == "week":
162        s -= timedelta(days=6)  # +1 current day already taken into account
163
164    # month (-30 day from 00:00:00 to the end of current day):
165    elif start.lower() == "month":
166        s -= timedelta(days=29)  # +1 current day already taken into account
167
168    # year (-365 day from 00:00:00 to the end of current day):
169    elif start.lower() == "year":
170        s -= timedelta(days=364)  # +1 current day already taken into account
171
172    # -N days ago to the end of current day:
173    elif start.startswith('-') and start[1:].isdigit():
174        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
175
176    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
177    else:
178        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
179        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
180
181    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
182    s = s.strftime(TKS_DATE_TIME_FORMAT)
183    e = e.strftime(TKS_DATE_TIME_FORMAT)
184
185    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
186
187    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 - how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 190class TinkoffBrokerServer:
 191    """
 192    This class implements methods to work with Tinkoff broker server.
 193
 194    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 195
 196    About `token`: https://tinkoff.github.io/investAPI/token/
 197    """
 198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 199        """
 200        Main class init.
 201
 202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 205        :param useCache: use default cache file with raw data to use instead of `iList`.
 206                         True by default. Cache is auto-update if new day has come.
 207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 208        :param defaultCache: path to default cache file. `dump.json` by default.
 209        """
 210        if token is None or not token:
 211            try:
 212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 214
 215            except KeyError:
 216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 217                raise Exception("Token required")
 218
 219        else:
 220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 222
 223        if accountId is None or not accountId:
 224            try:
 225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 227
 228            except KeyError:
 229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 230
 231        else:
 232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 234
 235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 237
 238        Latest version: https://pypi.org/project/tksbrokerapi/
 239        """
 240
 241        self.aliases = TKS_TICKER_ALIASES
 242        """Some aliases instead official tickers.
 243
 244        See also: `TKSEnums.TKS_TICKER_ALIASES`
 245        """
 246
 247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 248
 249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 250
 251        self.ticker = ""
 252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 253
 254        See also: `SearchByTicker()`, `SearchInstruments()`.
 255        """
 256
 257        self.figi = ""
 258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 259
 260        See also: `SearchByFIGI()`, `SearchInstruments()`.
 261        """
 262
 263        self.depth = 1
 264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 265
 266        See also: `GetCurrentPrices()`.
 267        """
 268
 269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 271
 272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 273        """
 274
 275        uLogger.debug("Broker API server: {}".format(self.server))
 276
 277        self.timeout = 15
 278        """Server operations timeout in seconds. Default: `15`.
 279
 280        See also: `SendAPIRequest()`.
 281        """
 282
 283        self.headers = {
 284            "Content-Type": "application/json",
 285            "accept": "application/json",
 286            "Authorization": "Bearer {}".format(self.token),
 287            "x-app-name": "Tim55667757.TKSBrokerAPI",
 288        }
 289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 290
 291        See also: `SendAPIRequest()`.
 292        """
 293
 294        self.body = None
 295        """Request body which send to broker server. Default: `None`.
 296
 297        See also: `SendAPIRequest()`.
 298        """
 299
 300        self.historyFile = None
 301        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 302
 303        See also: `History()`.
 304        """
 305
 306        self.htmlHistoryFile = "index.html"
 307        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 308
 309        See also: `ShowHistoryChart()`.
 310        """
 311
 312        self.instrumentsFile = "instruments.md"
 313        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 314
 315        See also: `ShowInstrumentsInfo()`.
 316        """
 317
 318        self.searchResultsFile = "search-results.md"
 319        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 320
 321        See also: `SearchInstruments()`.
 322        """
 323
 324        self.pricesFile = "prices.md"
 325        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 326
 327        See also: `GetListOfPrices()`.
 328        """
 329
 330        self.infoFile = "info.md"
 331        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 332
 333        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 334        """
 335
 336        self.bondsXLSXFile = "ext-bonds.xlsx"
 337        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 338        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 339
 340        See also: `ExtendBondsData()`.
 341        """
 342
 343        self.calendarFile = "calendar.md"
 344        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 345        
 346        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 347
 348        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 349        """
 350
 351        self.overviewFile = "overview.md"
 352        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 353
 354        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 355        """
 356
 357        self.overviewDigestFile = "overview-digest.md"
 358        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 359
 360        See also: `Overview()` with parameter `details="digest"`.
 361        """
 362
 363        self.overviewPositionsFile = "overview-positions.md"
 364        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 365
 366        See also: `Overview()` with parameter `details="positions"`.
 367        """
 368
 369        self.overviewOrdersFile = "overview-orders.md"
 370        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 371
 372        See also: `Overview()` with parameter `details="orders"`.
 373        """
 374
 375        self.overviewAnalyticsFile = "overview-analytics.md"
 376        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 377
 378        See also: `Overview()` with parameter `details="analytics"`.
 379        """
 380
 381        self.reportFile = "deals.md"
 382        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 383
 384        See also: `Deals()`.
 385        """
 386
 387        self.withdrawalLimitsFile = "limits.md"
 388        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 389
 390        See also: `OverviewLimits()` and `RequestLimits()`.
 391        """
 392
 393        self.userInfoFile = "user-info.md"
 394        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 395
 396        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 397        """
 398
 399        self.userAccountsFile = "accounts.md"
 400        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 401
 402        See also: `OverviewAccounts()`, `RequestAccounts()`.
 403        """
 404
 405        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 406        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 407
 408        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 409
 410        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 411        """
 412
 413        self.iList = None  # init iList for raw instruments data
 414        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 415        
 416        See also: `Listing()`, `DumpInstruments()`.
 417        """
 418
 419        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 420        if useCache:
 421            if os.path.exists(self.iListDumpFile):
 422                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 423                curTime = datetime.now(tzutc())
 424
 425                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 426                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 427
 428                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 429
 430                else:
 431                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 432
 433                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 434                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 435
 436            else:
 437                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 438                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 439
 440        else:
 441            self.iList = self.Listing()  # request new raw instruments data from broker server
 442            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 443
 444        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 445        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 446
 447        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 448        """
 449
 450    @staticmethod
 451    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 452        """
 453        Parse JSON from response string.
 454
 455        :param rawData: this is a string with JSON-formatted text.
 456        :param debug: if `True` then print more debug information.
 457        :return: JSON (dictionary), parsed from server response string.
 458        """
 459        if debug:
 460            uLogger.debug("Raw text body:")
 461            uLogger.debug(rawData)
 462
 463        responseJSON = json.loads(rawData) if rawData else {}
 464
 465        if debug:
 466            uLogger.debug("JSON formatted:")
 467            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 468                uLogger.debug(jsonLine)
 469
 470        return responseJSON
 471
 472    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 473        """
 474        Send GET or POST request to broker server and receive JSON object.
 475
 476        self.header: must be defining with dictionary of headers.
 477        self.body: if define then used as request body. None by default.
 478        self.timeout: global request timeout, 15 seconds by default.
 479        :param url: url with REST request.
 480        :param reqType: send "GET" or "POST" request. "GET" by default.
 481        :param retry: how many times retry after first request if an 5xx server errors occurred.
 482        :param pause: sleep time in seconds between retries.
 483        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 484        :return: response JSON (dictionary) from broker.
 485        """
 486        if reqType not in ("GET", "POST"):
 487            uLogger.error("You can define request type: 'GET' or 'POST'!")
 488            raise Exception("Incorrect value")
 489
 490        if debug:
 491            uLogger.debug("Request parameters:")
 492            uLogger.debug("    - REST API URL: {}".format(url))
 493            uLogger.debug("    - request type: {}".format(reqType))
 494            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 495            uLogger.debug("    - body: {}".format(self.body))
 496
 497        # fast hack to avoid all operations with some tickers/FIGI
 498        responseJSON = {}
 499        oK = True
 500        for item in self.exclude:
 501            if item in url:
 502                if debug:
 503                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 504
 505                oK = False
 506                break
 507
 508        if oK:
 509            counter = 0
 510            response = None
 511            errMsg = ""
 512
 513            while not response and counter <= retry:
 514                if reqType == "GET":
 515                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 516
 517                if reqType == "POST":
 518                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 519
 520                if debug:
 521                    uLogger.debug("Response:")
 522                    uLogger.debug("    - status code: {}".format(response.status_code))
 523                    uLogger.debug("    - reason: {}".format(response.reason))
 524                    uLogger.debug("    - body length: {}".format(len(response.text)))
 525                    uLogger.debug("    - headers: {}".format(response.headers))
 526
 527                # Server returns some headers:
 528                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 529                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 530                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 531                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 532                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 533                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 534                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 535                    sleep(rateLimitWait)
 536
 537                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 538                if 400 <= response.status_code < 500:
 539                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 540                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 541                    counter = retry + 1
 542
 543                if 500 <= response.status_code < 600:
 544                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 545                    uLogger.debug("    - not oK, {}".format(errMsg))
 546                    counter += 1
 547
 548                    if counter <= retry:
 549                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 550                        sleep(pause)
 551
 552            responseJSON = self._ParseJSON(response.text)
 553
 554            if errMsg:
 555                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 556                uLogger.error("    - not oK, {}".format(errMsg))
 557
 558        return responseJSON
 559
 560    def _IUpdater(self, iType: str) -> tuple:
 561        """
 562        Request instrument by type from server. See available API methods for instruments:
 563        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 564        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 565        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 566        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 567        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 568
 569        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 570        :return: tuple with iType name and list of available instruments of current type for defined user token.
 571        """
 572        result = []
 573
 574        if iType in TKS_INSTRUMENTS:
 575            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 576
 577            # all instruments have the same body in API v2 requests:
 578            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 579            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 580            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 581
 582        return iType, result
 583
 584    def _IWrapper(self, kwargs):
 585        """
 586        Wrapper runs instrument's update method `_IUpdater()`.
 587        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 588        """
 589        return self._IUpdater(**kwargs)
 590
 591    def Listing(self) -> dict:
 592        """
 593        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 594
 595        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 596        """
 597        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 598        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 599
 600        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 601        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 602        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 603
 604        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 605        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 606        poolUpdater.close()
 607
 608        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 609        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 610        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 611
 612        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 613        for iType in iList.keys():
 614            for ticker in iList[iType]:
 615                iList[iType][ticker]["type"] = iType
 616
 617                if "minPriceIncrement" in iList[iType][ticker].keys():
 618                    iList[iType][ticker]["step"] = NanoToFloat(
 619                        iList[iType][ticker]["minPriceIncrement"]["units"],
 620                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 621                    )
 622
 623                else:
 624                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 625
 626        return iList
 627
 628    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 629        """
 630        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 631
 632        See also: `DumpInstruments()`, `Listing()`.
 633
 634        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 635                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 636        """
 637        if self.iListDumpFile is None or not self.iListDumpFile:
 638            uLogger.error("Output name of dump file must be defined!")
 639            raise Exception("Filename required")
 640
 641        if not self.iList or forceUpdate:
 642            self.iList = self.Listing()
 643
 644        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 645
 646        # Save as XLSX with separated sheets for every type of instruments:
 647        with pd.ExcelWriter(
 648                path=xlsxDumpFile,
 649                date_format=TKS_DATE_FORMAT,
 650                datetime_format=TKS_DATE_TIME_FORMAT,
 651                mode="w",
 652        ) as writer:
 653            for iType in TKS_INSTRUMENTS:
 654                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 655                df = df[sorted(df)]  # sorted by column names
 656                df = df.applymap(
 657                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 658                    na_action="ignore",
 659                )  # converting numbers from nano-type to float in every cell
 660                df.to_excel(
 661                    writer,
 662                    sheet_name=iType,
 663                    encoding="UTF-8",
 664                    freeze_panes=(1, 1),
 665                )  # saving as XLSX-file with freeze first row and column as headers
 666
 667        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 668
 669    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 670        """
 671        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 672        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 673
 674        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 675
 676        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 677                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 678        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 679        """
 680        if self.iListDumpFile is None or not self.iListDumpFile:
 681            uLogger.error("Output name of dump file must be defined!")
 682            raise Exception("Filename required")
 683
 684        if not self.iList or forceUpdate:
 685            self.iList = self.Listing()
 686
 687        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 688        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 689            fH.write(jsonDump)
 690
 691        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 692
 693        return jsonDump
 694
 695    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 696        """
 697        Show information about one instrument defined by json data and prints it in Markdown format.
 698
 699        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 700
 701        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 702        :param show: if `True` then also printing information about instrument and its current price.
 703        :return: multilines text in Markdown format with information about one instrument.
 704        """
 705        splitLine = "|                                                             |                                                        |\n"
 706        infoText = ""
 707
 708        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 709            info = [
 710                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 711                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 712                "| Parameters                                                  | Values                                                 |\n",
 713                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 714                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 715                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 716            ]
 717
 718            if "sector" in iJSON.keys() and iJSON["sector"]:
 719                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 720
 721            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 722                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 723                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 724            )))
 725
 726            info.extend([
 727                splitLine,
 728                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 729                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 730            ])
 731
 732            if "isin" in iJSON.keys() and iJSON["isin"]:
 733                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 734
 735            if "classCode" in iJSON.keys():
 736                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 737
 738            info.extend([
 739                splitLine,
 740                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 741                splitLine,
 742                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 743                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 744                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 745            ])
 746
 747            if iJSON["figi"]:
 748                self.figi = iJSON["figi"]
 749                iJSON = iJSON | self.RequestTradingStatus()
 750
 751                info.extend([
 752                    splitLine,
 753                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 754                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 755                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 756                ])
 757
 758            info.append(splitLine)
 759
 760            if "type" in iJSON.keys() and iJSON["type"]:
 761                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 762
 763            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 764                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 765
 766            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 767                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 768
 769            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 770                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 771
 772            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 773                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 774
 775            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 776                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 777
 778            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 779                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 780
 781            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 782                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 783
 784            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 785                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 786
 787            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 788                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 789
 790            if "currency" in iJSON.keys():
 791                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 792
 793            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 794                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 795
 796            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 797                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 798
 799            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 800                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 801
 802            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 803                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 804
 805            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 806                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 807
 808            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 809                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 810
 811            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 812                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 813
 814            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 815                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 816
 817            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 818                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 819
 820            iExt = None
 821            if iJSON["type"] == "Bonds":
 822                info.extend([
 823                    splitLine,
 824                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 825                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 826                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 827                        iJSON["nominal"]["currency"],
 828                    )),
 829                ])
 830
 831                if "floatingCouponFlag" in iJSON.keys():
 832                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 833
 834                if "amortizationFlag" in iJSON.keys():
 835                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 836
 837                info.append(splitLine)
 838
 839                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 840                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 841
 842                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 843
 844                info.extend([
 845                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 846                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 847                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 848                ])
 849
 850                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 851                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 852                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 853                        iJSON["aciValue"]["currency"]
 854                    )))
 855
 856            if "currentPrice" in iJSON.keys():
 857                info.append(splitLine)
 858
 859                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 860                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 861
 862                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 863                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 864                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 865                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 866                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 867
 868                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 869                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 870
 871                info.extend([
 872                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 873                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 874                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 875                    )),
 876                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 877                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 878                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 879                    )),
 880                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 881                        "{:.2f}%{}".format(
 882                            iJSON["currentPrice"]["changes"],
 883                            " ({}{:.2f} {})".format(
 884                                "+" if bondChangesDelta > 0 else "",
 885                                bondChangesDelta,
 886                                aciCurrency
 887                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 888                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 889                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 890                                currency
 891                            ),
 892                        )
 893                    ),
 894                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 895                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 897                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 898                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 899                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 900                    )),
 901                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 902                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 903                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 904                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 905                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 906                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 907                    )),
 908                ])
 909
 910            if "lot" in iJSON.keys():
 911                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 912
 913            if "step" in iJSON.keys() and iJSON["step"] != 0:
 914                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 915
 916            # Add bond payment calendar:
 917            if iJSON["type"] == "Bonds":
 918                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 919                info.extend(["\n", strCalendar])
 920
 921            infoText += "".join(info)
 922
 923            if show:
 924                uLogger.info("{}".format(infoText))
 925
 926            else:
 927                uLogger.debug("{}".format(infoText))
 928
 929            if self.infoFile is not None:
 930                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 931                    fH.write(infoText)
 932
 933                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 934
 935        return infoText
 936
 937    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 938        """
 939        Search and return raw broker's information about instrument by its ticker.
 940        `ticker` must be defined! If debug=True then print all debug messages.
 941
 942        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 943        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 944        :param debug: if `True` then print all debug console messages.
 945        :return: JSON formatted data with information about instrument.
 946        """
 947        tickerJSON = {}
 948        if debug:
 949            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 950
 951        if not self.ticker:
 952            uLogger.warning("self.ticker variable is not be empty!")
 953
 954        else:
 955            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 956                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 957                raise Exception("Instrument not allowed")
 958
 959            if not self.iList:
 960                self.iList = self.Listing()
 961
 962            if self.ticker in self.iList["Shares"].keys():
 963                tickerJSON = self.iList["Shares"][self.ticker]
 964                if debug:
 965                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 966
 967            elif self.ticker in self.iList["Currencies"].keys():
 968                tickerJSON = self.iList["Currencies"][self.ticker]
 969                if debug:
 970                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 971
 972            elif self.ticker in self.iList["Bonds"].keys():
 973                tickerJSON = self.iList["Bonds"][self.ticker]
 974                if debug:
 975                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 976
 977            elif self.ticker in self.iList["Etfs"].keys():
 978                tickerJSON = self.iList["Etfs"][self.ticker]
 979                if debug:
 980                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 981
 982            elif self.ticker in self.iList["Futures"].keys():
 983                tickerJSON = self.iList["Futures"][self.ticker]
 984                if debug:
 985                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 986
 987        if tickerJSON:
 988            self.figi = tickerJSON["figi"]
 989
 990            if requestPrice:
 991                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 992
 993                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 994                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 995
 996                else:
 997                    tickerJSON["currentPrice"]["changes"] = 0
 998
 999            if show:
1000                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1001
1002        else:
1003            if show:
1004                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1005
1006        return tickerJSON
1007
1008    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1009        """
1010        Search and return raw broker's information about instrument by its FIGI.
1011        `figi` must be defined! If debug=True then print all debug messages.
1012
1013        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1014        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1015        :param debug: if `True` then print all debug console messages.
1016        :return: JSON formatted data with information about instrument.
1017        """
1018        figiJSON = {}
1019        if debug:
1020            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1021
1022        if not self.figi:
1023            uLogger.warning("self.figi variable is not be empty!")
1024
1025        else:
1026            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1027                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1028                raise Exception("Instrument not allowed")
1029
1030            if not self.iList:
1031                self.iList = self.Listing()
1032
1033            for item in self.iList["Shares"].keys():
1034                if self.figi == self.iList["Shares"][item]["figi"]:
1035                    figiJSON = self.iList["Shares"][item]
1036
1037                    if debug:
1038                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1039
1040                    break
1041
1042            if not figiJSON:
1043                for item in self.iList["Currencies"].keys():
1044                    if self.figi == self.iList["Currencies"][item]["figi"]:
1045                        figiJSON = self.iList["Currencies"][item]
1046
1047                        if debug:
1048                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1049
1050                        break
1051
1052            if not figiJSON:
1053                for item in self.iList["Bonds"].keys():
1054                    if self.figi == self.iList["Bonds"][item]["figi"]:
1055                        figiJSON = self.iList["Bonds"][item]
1056
1057                        if debug:
1058                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1059
1060                        break
1061
1062            if not figiJSON:
1063                for item in self.iList["Etfs"].keys():
1064                    if self.figi == self.iList["Etfs"][item]["figi"]:
1065                        figiJSON = self.iList["Etfs"][item]
1066
1067                        if debug:
1068                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1069
1070                        break
1071
1072            if not figiJSON:
1073                for item in self.iList["Futures"].keys():
1074                    if self.figi == self.iList["Futures"][item]["figi"]:
1075                        figiJSON = self.iList["Futures"][item]
1076
1077                        if debug:
1078                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1079
1080                        break
1081
1082        if figiJSON:
1083            self.figi = figiJSON["figi"]
1084            self.ticker = figiJSON["ticker"]
1085
1086            if requestPrice:
1087                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1088
1089                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1090                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1091
1092                else:
1093                    figiJSON["currentPrice"]["changes"] = 0
1094
1095            if show:
1096                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1097
1098        else:
1099            if show:
1100                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1101
1102        return figiJSON
1103
1104    def GetCurrentPrices(self, show: bool = True) -> dict:
1105        """
1106        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1107        `{"buy": [{"price": 1243.8, "quantity": 193},
1108                  {"price": 1244.0, "quantity": 168},
1109                  {"price": 1244.8, "quantity": 5},
1110                  {"price": 1245.0, "quantity": 61},
1111                  {"price": 1245.4, "quantity": 60}],
1112          "sell": [{"price": 1243.6, "quantity": 8},
1113                   {"price": 1242.6, "quantity": 10},
1114                   {"price": 1242.4, "quantity": 18},
1115                   {"price": 1242.2, "quantity": 50},
1116                   {"price": 1242.0, "quantity": 113}],
1117          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1118        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1119        - sell: list of dicts with Buyers prices,
1120            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1121            - quantity: volume value by current price in lots,
1122        - limitUp: current trade session limit price, maximum,
1123        - limitDown: current trade session limit price, minimum,
1124        - lastPrice: last deal price of the instrument,
1125        - closePrice: previous trade session close price of the instrument.
1126
1127        See also: `SearchByTicker()` and `SearchByFIGI()`.
1128        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1129        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1130
1131        :param show: if `True` then print DOM to log and console.
1132        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1133                 If an error occurred then returns an empty record:
1134                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1135        """
1136        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1137
1138        if self.depth < 1:
1139            uLogger.error("Depth of Market (DOM) must be >=1!")
1140            raise Exception("Incorrect value")
1141
1142        if not (self.ticker or self.figi):
1143            uLogger.error("self.ticker or self.figi variables must be defined!")
1144            raise Exception("Ticker or FIGI required")
1145
1146        if self.ticker and not self.figi:
1147            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1148            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1149
1150        if not self.ticker and self.figi:
1151            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1152            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1153
1154        if not self.figi:
1155            uLogger.error("FIGI is not defined!")
1156            raise Exception("Ticker or FIGI required")
1157
1158        else:
1159            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1160
1161            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1162            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1163            self.body = str({"figi": self.figi, "depth": self.depth})
1164            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1165
1166            if pricesResponse:
1167                # list of dicts with sellers orders:
1168                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1169
1170                # list of dicts with buyers orders:
1171                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1172
1173                # max price of instrument at this time:
1174                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1175
1176                # min price of instrument at this time:
1177                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1178
1179                # last price of deal with instrument:
1180                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1181
1182                # last close price of instrument:
1183                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1184
1185            else:
1186                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1187                uLogger.debug("Server response: {}".format(pricesResponse))
1188
1189            if show:
1190                if prices["buy"] or prices["sell"]:
1191                    info = [
1192                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1193                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1194                            self.ticker,
1195                            self.figi,
1196                            self.depth,
1197                        ),
1198                        "-" * 60, "\n",
1199                        "             Orders of Buyers | Orders of Sellers\n",
1200                        "-" * 60, "\n",
1201                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1202                        "-" * 60, "\n",
1203                    ]
1204
1205                    if not prices["buy"]:
1206                        info.append("                              | No orders!\n")
1207                        sumBuy = 0
1208
1209                    else:
1210                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1211                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1212                        for item in maxMinSorted:
1213                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1214
1215                    if not prices["sell"]:
1216                        info.append("No orders!                    |\n")
1217                        sumSell = 0
1218
1219                    else:
1220                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1221                        for item in prices["sell"]:
1222                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1223
1224                    info.extend([
1225                        "-" * 60, "\n",
1226                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1227                        "-" * 60, "\n",
1228                    ])
1229
1230                    infoText = "".join(info)
1231
1232                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1233
1234                else:
1235                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1236
1237        return prices
1238
1239    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1240        """
1241        This method get and show information about all available broker instruments for current user account.
1242        If `instrumentsFile` string is not empty then also save information to this file.
1243
1244        :param show: if `True` then print results to console, if `False` - print only to file.
1245        :return: multi-lines string with all available broker instruments
1246        """
1247        if not self.iList:
1248            self.iList = self.Listing()
1249
1250        info = [
1251            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1252            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1253        ]
1254
1255        # add instruments count by type:
1256        for iType in self.iList.keys():
1257            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1258
1259        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1260        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1261
1262        # generating info tables with all instruments by type:
1263        for iType in self.iList.keys():
1264            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1265
1266            for instrument in self.iList[iType].keys():
1267                iName = self.iList[iType][instrument]["name"]  # instrument's name
1268                if len(iName) > 57:
1269                    iName = "{}...".format(iName[:54])  # right trim for a long string
1270
1271                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1272                    self.iList[iType][instrument]["ticker"],
1273                    iName,
1274                    self.iList[iType][instrument]["figi"],
1275                    self.iList[iType][instrument]["currency"],
1276                    self.iList[iType][instrument]["lot"],
1277                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1278                ))
1279
1280        infoText = "".join(info)
1281
1282        if show:
1283            uLogger.info(infoText)
1284
1285        if self.instrumentsFile:
1286            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1287                fH.write(infoText)
1288
1289            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1290
1291        return infoText
1292
1293    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1294        """
1295        This method search and show information about instruments by part of its ticker, FIGI or name.
1296        If `searchResultsFile` string is not empty then also save information to this file.
1297
1298        :param pattern: string with part of ticker, FIGI or instrument's name.
1299        :param show: if `True` then print results to console, if `False` - return list of result only.
1300        :return: list of dictionaries with all found instruments.
1301        """
1302        if not self.iList:
1303            self.iList = self.Listing()
1304
1305        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1306        compiledPattern = re.compile(pattern, re.IGNORECASE)
1307
1308        for iType in self.iList:
1309            for instrument in self.iList[iType].values():
1310                searchResult = compiledPattern.search(" ".join(
1311                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1312                ))
1313
1314                if searchResult:
1315                    searchResults[iType][instrument["ticker"]] = instrument
1316
1317        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1318        info = [
1319            "# Search results\n\n",
1320            "* **Search pattern:** [{}]\n".format(pattern),
1321            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1322            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1323        ]
1324        infoShort = info[:]
1325
1326        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1327        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1328        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1329
1330        if resultsLen == 0:
1331            info.append("\nNo results\n")
1332            infoShort.append("\nNo results\n")
1333            uLogger.warning("No results. Try changing your search pattern.")
1334
1335        else:
1336            for iType in searchResults:
1337                iTypeValuesCount = len(searchResults[iType].values())
1338                if iTypeValuesCount > 0:
1339                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1340                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1341
1342                    for instrument in searchResults[iType].values():
1343                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1344                            instrument["type"],
1345                            instrument["ticker"],
1346                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1347                            instrument["figi"],
1348                        ))
1349
1350                    if iTypeValuesCount <= 5:
1351                        infoShort.extend(info[-iTypeValuesCount:])
1352
1353                    else:
1354                        infoShort.extend(info[-5:])
1355                        infoShort.append(skippedLine)
1356
1357        infoText = "".join(info)
1358        infoTextShort = "".join(infoShort)
1359
1360        if show:
1361            uLogger.info(infoTextShort)
1362            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1363
1364        if self.searchResultsFile:
1365            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1366                fH.write(infoText)
1367
1368            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1369
1370        return searchResults
1371
1372    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1373        """
1374        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1375
1376        :param instruments: list of strings with tickers or FIGIs.
1377        :return: list with unique instrument FIGIs only.
1378        """
1379        requestedInstruments = []
1380        for iName in instruments:
1381            if iName not in self.aliases.keys():
1382                if iName not in requestedInstruments:
1383                    requestedInstruments.append(iName)
1384
1385            else:
1386                if iName not in requestedInstruments:
1387                    if self.aliases[iName] not in requestedInstruments:
1388                        requestedInstruments.append(self.aliases[iName])
1389
1390        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1391
1392        onlyUniqueFIGIs = []
1393        for iName in requestedInstruments:
1394            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1395                continue
1396
1397            self.ticker = iName
1398            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1399
1400            if not iData:
1401                self.ticker = ""
1402                self.figi = iName
1403
1404                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1405
1406                if not iData:
1407                    self.figi = ""
1408                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1409
1410            if iData and iData["figi"] not in onlyUniqueFIGIs:
1411                onlyUniqueFIGIs.append(iData["figi"])
1412
1413        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1414
1415        return onlyUniqueFIGIs
1416
1417    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1418        """
1419        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1420        See limits: https://tinkoff.github.io/investAPI/limits/
1421        If `pricesFile` string is not empty then also save information to this file.
1422
1423        :param instruments: list of strings with tickers or FIGIs.
1424        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1425        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1426                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1427        """
1428        if instruments is None or not instruments:
1429            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1430            raise Exception("Ticker or FIGI required")
1431
1432        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1433
1434        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1435
1436        iList = []  # trying to get info and current prices about all unique instruments:
1437        for self.figi in onlyUniqueFIGIs:
1438            iData = self.SearchByFIGI(requestPrice=True)
1439            iList.append(iData)
1440
1441        self.ShowListOfPrices(iList, show)
1442
1443        return iList
1444
1445    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1446        """
1447        Show table contains current prices of given instruments.
1448
1449        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1450                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1451        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1452        :return: multilines text in Markdown format as a table contains current prices.
1453        """
1454        infoText = ""
1455
1456        if show or self.pricesFile:
1457            info = [
1458                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1459                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1460                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1461            ]
1462
1463            for item in iList:
1464                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1465                    item["ticker"],
1466                    item["figi"],
1467                    item["type"],
1468                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1469                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1470                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1471                    "{} / {}".format(
1472                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1473                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1474                    ),
1475                    "{} / {}".format(
1476                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1477                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1478                    ),
1479                    item["currency"],
1480                ))
1481
1482            infoText = "".join(info)
1483
1484            if show:
1485                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1486
1487            if self.pricesFile:
1488                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1489                    fH.write(infoText)
1490
1491                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1492
1493        return infoText
1494
1495    def RequestTradingStatus(self) -> dict:
1496        """
1497        Requesting trading status for the instrument defined by `figi` variable.
1498        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1499        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1500
1501        :return: dictionary with trading status attributes. Response example:
1502                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1503                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1504        """
1505        if self.figi is None or not self.figi:
1506            uLogger.error("Variable `figi` must be defined for using this method!")
1507            raise Exception("FIGI required")
1508
1509        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1510
1511        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1512        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1513        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1514
1515        uLogger.debug("Records about current trading status successfully received")
1516
1517        return tradingStatus
1518
1519    def RequestPortfolio(self) -> dict:
1520        """
1521        Requesting actual user's portfolio for current `accountId`.
1522        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1523        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1524
1525        :return: dictionary with user's portfolio.
1526        """
1527        if self.accountId is None or not self.accountId:
1528            uLogger.error("Variable `accountId` must be defined for using this method!")
1529            raise Exception("Account ID required")
1530
1531        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1532
1533        self.body = str({"accountId": self.accountId})
1534        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1535        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1536
1537        uLogger.debug("Records about user's portfolio successfully received")
1538
1539        return rawPortfolio
1540
1541    def RequestPositions(self) -> dict:
1542        """
1543        Requesting open positions by currencies and instruments for current `accountId`.
1544        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1545        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1546
1547        :return: dictionary with open positions by instruments.
1548        """
1549        if self.accountId is None or not self.accountId:
1550            uLogger.error("Variable `accountId` must be defined for using this method!")
1551            raise Exception("Account ID required")
1552
1553        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1554
1555        self.body = str({"accountId": self.accountId})
1556        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1557        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1558
1559        uLogger.debug("Records about current open positions successfully received")
1560
1561        return rawPositions
1562
1563    def RequestPendingOrders(self) -> list:
1564        """
1565        Requesting current actual pending orders for current `accountId`.
1566        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1567        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1568
1569        :return: list of dictionaries with pending orders.
1570        """
1571        if self.accountId is None or not self.accountId:
1572            uLogger.error("Variable `accountId` must be defined for using this method!")
1573            raise Exception("Account ID required")
1574
1575        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1576
1577        self.body = str({"accountId": self.accountId})
1578        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1579        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1580
1581        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1582
1583        return rawOrders
1584
1585    def RequestStopOrders(self) -> list:
1586        """
1587        Requesting current actual stop orders for current `accountId`.
1588        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1589        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1590
1591        :return: list of dictionaries with stop orders.
1592        """
1593        if self.accountId is None or not self.accountId:
1594            uLogger.error("Variable `accountId` must be defined for using this method!")
1595            raise Exception("Account ID required")
1596
1597        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1598
1599        self.body = str({"accountId": self.accountId})
1600        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1601        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1602
1603        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1604
1605        return rawStopOrders
1606
1607    def Overview(self, show: bool = False, details: str = "full") -> dict:
1608        """
1609        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1610        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1611        are defined then also save information to file.
1612
1613        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1614        many requests about the state of the portfolio, and then, based on the received data, a large number
1615        of calculation and statistics are collected.
1616
1617        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1618        :param details: how detailed should the information be? You should specify one of strings:
1619                        `full` - shows full available information about portfolio status (by default),
1620                        `positions` - shows only open positions,
1621                        `digest` - show a short digest of the portfolio status,
1622                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1623                        `orders` - shows only sections of open limits and stop orders.
1624        :return: dictionary with client's raw portfolio and some statistics.
1625        """
1626        if self.accountId is None or not self.accountId:
1627            uLogger.error("Variable `accountId` must be defined for using this method!")
1628            raise Exception("Account ID required")
1629
1630        view = {
1631            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1632                "headers": {},  # list of dictionaries, response headers without "positions" section
1633                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1634                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1635                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1636                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1637                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1638                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1639                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1640                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1641                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1642            },
1643            "stat": {  # --- some statistics calculated using "raw" sections:
1644                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1645                "availableRUB": 0.,  # available rubles (without other currencies)
1646                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1647                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1648                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1649                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1650                "sharesCostRUB": 0.,  # costs of all shares in RUB
1651                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1652                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1653                "futuresCostRUB": 0.,  # costs of all futures in RUB
1654                "Currencies": [],  # list of dictionaries of all currencies statistics
1655                "Shares": [],  # list of dictionaries of all shares statistics
1656                "Bonds": [],  # list of dictionaries of all bonds statistics
1657                "Etfs": [],  # list of dictionaries of all etfs statistics
1658                "Futures": [],  # list of dictionaries of all futures statistics
1659                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1660                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1661                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1662                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1663                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1664            },
1665            "analytics": {  # --- some analytics of portfolio:
1666                "distrByAssets": {},  # portfolio distribution by assets
1667                "distrByCompanies": {},  # portfolio distribution by companies
1668                "distrBySectors": {},  # portfolio distribution by sectors
1669                "distrByCurrencies": {},  # portfolio distribution by currencies
1670                "distrByCountries": {},  # portfolio distribution by countries
1671            }
1672        }
1673
1674        details = details.lower()
1675        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1676        if details not in availableDetails:
1677            details = "full"
1678            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1679
1680        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1681
1682        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1683        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1684        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1685        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1686
1687        # save response headers without "positions" section:
1688        for key in portfolioResponse.keys():
1689            if key != "positions":
1690                view["raw"]["headers"][key] = portfolioResponse[key]
1691
1692            else:
1693                continue
1694
1695        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1696        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1697        for item in portfolioResponse["positions"]:
1698            if item["instrumentType"] == "currency":
1699                self.figi = item["figi"]
1700                curr = self.SearchByFIGI(requestPrice=False)
1701
1702                # current price of currency in RUB:
1703                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1704                    "name": curr["name"],
1705                    "currentPrice": NanoToFloat(
1706                        item["currentPrice"]["units"],
1707                        item["currentPrice"]["nano"]
1708                    ),
1709                }
1710
1711                view["raw"]["Currencies"].append(item)
1712
1713            elif item["instrumentType"] == "share":
1714                view["raw"]["Shares"].append(item)
1715
1716            elif item["instrumentType"] == "bond":
1717                view["raw"]["Bonds"].append(item)
1718
1719            elif item["instrumentType"] == "etf":
1720                view["raw"]["Etfs"].append(item)
1721
1722            elif item["instrumentType"] == "futures":
1723                view["raw"]["Futures"].append(item)
1724
1725            else:
1726                continue
1727
1728        # how many volume of currencies (by ISO currency name) are blocked:
1729        for item in view["raw"]["positions"]["blocked"]:
1730            blocked = NanoToFloat(item["units"], item["nano"])
1731            if blocked > 0:
1732                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1733
1734        # how many volume of instruments (by FIGI) are blocked:
1735        for item in view["raw"]["positions"]["securities"]:
1736            blocked = int(item["blocked"])
1737            if blocked > 0:
1738                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1739
1740        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1741
1742        if "rub" in allBlocked.keys():
1743            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1744
1745        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1746        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1747        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1748        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1749        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1750        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1751        view["stat"]["portfolioCostRUB"] = sum([
1752            view["stat"]["allCurrenciesCostRUB"],
1753            view["stat"]["sharesCostRUB"],
1754            view["stat"]["bondsCostRUB"],
1755            view["stat"]["etfsCostRUB"],
1756            view["stat"]["futuresCostRUB"],
1757        ])
1758
1759        # --- calculating some portfolio statistics:
1760        byComp = {}  # distribution by companies
1761        bySect = {}  # distribution by sectors
1762        byCurr = {}  # distribution by currencies (include RUB)
1763        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1764        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1765
1766        for item in portfolioResponse["positions"]:
1767            self.figi = item["figi"]
1768            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1769
1770            if instrument:
1771                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1772                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1773
1774                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1775                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1776
1777                else:
1778                    blocked = 0
1779
1780                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1781                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1782                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1783                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1784                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1785                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1786                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1787                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1788                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1789                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1790                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1791                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1792
1793                statData = {
1794                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1795                    "ticker": instrument["ticker"],  # ticker by FIGI
1796                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1797                    "volume": volume,  # available volume of instrument
1798                    "lots": lots,  # volume in lots of instrument
1799                    "direction": direction,  # direction of an instrument's position: short or long
1800                    "blocked": blocked,  # blocked volume of currency or instrument
1801                    "currentPrice": curPrice,  # current instrument's price in basic asset
1802                    "average": average,  # current average position price
1803                    "cost": cost,  # current cost of all volume of instrument in basic asset
1804                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1805                    "costRUB": costRUB,  # cost of instrument in ruble
1806                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1807                    "profit": profit,  # expected profit at current moment
1808                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1809                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1810                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1811                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1812                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1813                    "step": instrument["step"],  # minimum price increment
1814                }
1815
1816                # adding distribution by unique countries:
1817                if statData["country"] not in byCountry.keys():
1818                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1819
1820                else:
1821                    byCountry[statData["country"]]["cost"] += costRUB
1822                    byCountry[statData["country"]]["percent"] += percentCostRUB
1823
1824                if item["instrumentType"] != "currency":
1825                    # adding distribution by unique companies:
1826                    if statData["name"]:
1827                        if statData["name"] not in byComp.keys():
1828                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1829
1830                        else:
1831                            byComp[statData["name"]]["cost"] += costRUB
1832                            byComp[statData["name"]]["percent"] += percentCostRUB
1833
1834                    # adding distribution by unique sectors:
1835                    if statData["sector"] not in bySect.keys():
1836                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1837
1838                    else:
1839                        bySect[statData["sector"]]["cost"] += costRUB
1840                        bySect[statData["sector"]]["percent"] += percentCostRUB
1841
1842                # adding distribution by unique currencies:
1843                if currency not in byCurr.keys():
1844                    byCurr[currency] = {
1845                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1846                        "cost": costRUB,
1847                        "percent": percentCostRUB
1848                    }
1849
1850                else:
1851                    byCurr[currency]["cost"] += costRUB
1852                    byCurr[currency]["percent"] += percentCostRUB
1853
1854                # saving statistics for every instrument:
1855                if item["instrumentType"] == "currency":
1856                    view["stat"]["Currencies"].append(statData)
1857
1858                    # update dict with free funds for trading (total - blocked) by currencies
1859                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1860                    view["stat"]["funds"][currency] = {
1861                        "total": volume,
1862                        "totalCostRUB": costRUB,  # total volume cost in rubles
1863                        "free": volume - blocked,
1864                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1865                    }
1866
1867                elif item["instrumentType"] == "share":
1868                    view["stat"]["Shares"].append(statData)
1869
1870                elif item["instrumentType"] == "bond":
1871                    view["stat"]["Bonds"].append(statData)
1872
1873                elif item["instrumentType"] == "etf":
1874                    view["stat"]["Etfs"].append(statData)
1875
1876                elif item["instrumentType"] == "Futures":
1877                    view["stat"]["Futures"].append(statData)
1878
1879                else:
1880                    continue
1881
1882        # total changes in Russian Ruble:
1883        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1884        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1885        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1886        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1887        view["stat"]["funds"]["rub"] = {
1888            "total": view["stat"]["availableRUB"],
1889            "totalCostRUB": view["stat"]["availableRUB"],
1890            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1891            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1892        }
1893
1894        # --- pending orders sector data:
1895        uniquePendingOrders = []
1896        uniquePendingOrdersFIGIs = []
1897        for item in view["raw"]["orders"]:
1898            if item["figi"] not in uniquePendingOrdersFIGIs:
1899                uniquePendingOrdersFIGIs.append(item["figi"])
1900                uniquePendingOrders.append(item)
1901
1902        for item in uniquePendingOrders:
1903            self.figi = item["figi"]
1904            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1905
1906            if instrument:
1907                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1908                orderType = TKS_ORDER_TYPES[item["orderType"]]
1909                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1910                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1911
1912                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1913                if item["direction"] == "ORDER_DIRECTION_BUY":
1914                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1915
1916                else:
1917                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1918
1919                # requested price for order execution:
1920                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1921
1922                # necessary changes in percent to reach target from current price:
1923                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1924
1925                view["stat"]["orders"].append({
1926                    "orderID": item["orderId"],  # orderId number parameter of current order
1927                    "figi": item["figi"],  # FIGI identification
1928                    "ticker": instrument["ticker"],  # ticker name by FIGI
1929                    "lotsRequested": item["lotsRequested"],  # requested lots value
1930                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1931                    "currentPrice": lastPrice,  # current instrument's price for defined action
1932                    "targetPrice": target,  # requested price for order execution in base currency
1933                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1934                    "percentChanges": changes,  # changes in percent to target from current price
1935                    "currency": item["currency"],  # instrument's currency name
1936                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1937                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1938                    "status": orderState,  # order status from TKS_ORDER_STATES
1939                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1940                })
1941
1942        # --- stop orders sector data:
1943        uniqueStopOrders = []
1944        uniqueStopOrdersFIGIs = []
1945        for item in view["raw"]["stopOrders"]:
1946            if item["figi"] not in uniqueStopOrdersFIGIs:
1947                uniqueStopOrdersFIGIs.append(item["figi"])
1948                uniqueStopOrders.append(item)
1949
1950        for item in uniqueStopOrders:
1951            self.figi = item["figi"]
1952            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1953
1954            if instrument:
1955                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1956                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1957                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1958
1959                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1960                if "expirationTime" in item.keys():
1961                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1962                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1963
1964                else:
1965                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1966                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1967
1968                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1969                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1970                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1971
1972                else:
1973                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1974
1975                # requested price when stop-order executed:
1976                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1977
1978                # price for limit-order, set up when stop-order executed:
1979                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1980
1981                # necessary changes in percent to reach target from current price:
1982                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1983
1984                view["stat"]["stopOrders"].append({
1985                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1986                    "figi": item["figi"],  # FIGI identification
1987                    "ticker": instrument["ticker"],  # ticker name by FIGI
1988                    "lotsRequested": item["lotsRequested"],  # requested lots value
1989                    "currentPrice": lastPrice,  # current instrument's price for defined action
1990                    "targetPrice": target,  # requested price for stop-order execution in base currency
1991                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1992                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1993                    "percentChanges": changes,  # changes in percent to target from current price
1994                    "currency": item["currency"],  # instrument's currency name
1995                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1996                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1997                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1998                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1999                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2000                })
2001
2002        # --- calculating data for analytics section:
2003        # portfolio distribution by assets:
2004        view["analytics"]["distrByAssets"] = {
2005            "Ruble": {
2006                "uniques": 1,
2007                "cost": view["stat"]["availableRUB"],
2008                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2009            },
2010            "Currencies": {
2011                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2012                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2013                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2014            },
2015            "Shares": {
2016                "uniques": len(view["stat"]["Shares"]),
2017                "cost": view["stat"]["sharesCostRUB"],
2018                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2019            },
2020            "Bonds": {
2021                "uniques": len(view["stat"]["Bonds"]),
2022                "cost": view["stat"]["bondsCostRUB"],
2023                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2024            },
2025            "Etfs": {
2026                "uniques": len(view["stat"]["Etfs"]),
2027                "cost": view["stat"]["etfsCostRUB"],
2028                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2029            },
2030            "Futures": {
2031                "uniques": len(view["stat"]["Futures"]),
2032                "cost": view["stat"]["futuresCostRUB"],
2033                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2034            },
2035        }
2036
2037        # portfolio distribution by companies:
2038        view["analytics"]["distrByCompanies"]["All money cash"] = {
2039            "ticker": "",
2040            "cost": view["stat"]["allCurrenciesCostRUB"],
2041            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2042        }
2043        view["analytics"]["distrByCompanies"].update(byComp)
2044
2045        # portfolio distribution by sectors:
2046        view["analytics"]["distrBySectors"]["All money cash"] = {
2047            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2048            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2049        }
2050        view["analytics"]["distrBySectors"].update(bySect)
2051
2052        # portfolio distribution by currencies:
2053        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2054            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2055            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2056
2057        view["analytics"]["distrByCurrencies"].update(byCurr)
2058        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2059        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2060
2061        # portfolio distribution by countries:
2062        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2063            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2064            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2065
2066        view["analytics"]["distrByCountries"].update(byCountry)
2067        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2068        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2069
2070        # --- Prepare text statistics overview in human-readable:
2071        if show:
2072            # Whatever the value `details`, header not changes:
2073            info = [
2074                "# Client's portfolio\n\n",
2075                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2076                "* **Account ID:** [{}]\n".format(self.accountId),
2077            ]
2078
2079            if details in ["full", "positions", "digest"]:
2080                info.extend([
2081                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2082                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2083                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2084                        view["stat"]["totalChangesRUB"],
2085                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2086                        view["stat"]["totalChangesPercentRUB"],
2087                    ),
2088                ])
2089
2090            if details in ["full", "positions"]:
2091                info.extend([
2092                    "## Open positions\n\n",
2093                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2094                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2095                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2096                        "{:.2f} ({:.2f}) rub".format(
2097                            view["stat"]["availableRUB"],
2098                            view["stat"]["blockedRUB"],
2099                        )
2100                    )
2101                ])
2102
2103                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2104                    return [
2105                        "|                             |                                 |          |              |              |                     |                              |\n",
2106                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2107                            noTradeStr if noTradeStr else typeStr,
2108                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2109                        ),
2110                    ]
2111
2112                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2113                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2114                        "{} [{}]".format(data["ticker"], data["figi"]),
2115                        "{:.2f} ({:.2f}) {}".format(
2116                            data["volume"],
2117                            data["blocked"],
2118                            data["currency"],
2119                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2120                            data["volume"],
2121                            data["blocked"],
2122                        ),
2123                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2124                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2125                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2126                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2127                        "{}{:.2f} {} ({}{:.2f}%)".format(
2128                            "+" if data["profit"] > 0 else "",
2129                            data["profit"], data["baseCurrencyName"],
2130                            "+" if data["percentProfit"] > 0 else "",
2131                            data["percentProfit"],
2132                        ),
2133                    )
2134
2135                # --- Show currencies section:
2136                if view["stat"]["Currencies"]:
2137                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2138                    for item in view["stat"]["Currencies"]:
2139                        info.append(_InfoStr(item, showCurrencyName=True))
2140
2141                else:
2142                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2143
2144                # --- Show shares section:
2145                if view["stat"]["Shares"]:
2146                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2147
2148                    for item in view["stat"]["Shares"]:
2149                        info.append(_InfoStr(item))
2150
2151                else:
2152                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2153
2154                # --- Show bonds section:
2155                if view["stat"]["Bonds"]:
2156                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2157
2158                    for item in view["stat"]["Bonds"]:
2159                        info.append(_InfoStr(item))
2160
2161                else:
2162                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2163
2164                # --- Show etfs section:
2165                if view["stat"]["Etfs"]:
2166                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2167
2168                    for item in view["stat"]["Etfs"]:
2169                        info.append(_InfoStr(item))
2170
2171                else:
2172                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2173
2174                # --- Show futures section:
2175                if view["stat"]["Futures"]:
2176                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2177
2178                    for item in view["stat"]["Futures"]:
2179                        info.append(_InfoStr(item))
2180
2181                else:
2182                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2183
2184            if details in ["full", "orders"]:
2185                # --- Show pending orders section:
2186                if view["stat"]["orders"]:
2187                    info.extend([
2188                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2189                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2190                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2191                    ])
2192
2193                    for item in view["stat"]["orders"]:
2194                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2195                            "{} [{}]".format(item["ticker"], item["figi"]),
2196                            item["orderID"],
2197                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2198                            "{} {} ({}{:.2f}%)".format(
2199                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2200                                item["baseCurrencyName"],
2201                                "+" if item["percentChanges"] > 0 else "",
2202                                float(item["percentChanges"]),
2203                            ),
2204                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2205                            item["action"],
2206                            item["type"],
2207                            item["date"],
2208                        ))
2209
2210                else:
2211                    info.append("\n## Total pending limit-orders: 0\n")
2212
2213                # --- Show stop orders section:
2214                if view["stat"]["stopOrders"]:
2215                    info.extend([
2216                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2217                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2218                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2219                    ])
2220
2221                    for item in view["stat"]["stopOrders"]:
2222                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2223                            "{} [{}]".format(item["ticker"], item["figi"]),
2224                            item["orderID"],
2225                            item["lotsRequested"],
2226                            "{} {} ({}{:.2f}%)".format(
2227                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2228                                item["baseCurrencyName"],
2229                                "+" if item["percentChanges"] > 0 else "",
2230                                float(item["percentChanges"]),
2231                            ),
2232                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2233                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2234                            item["action"],
2235                            item["type"],
2236                            item["expType"],
2237                            item["createDate"],
2238                            item["expDate"],
2239                        ))
2240
2241                else:
2242                    info.append("\n## Total stop-orders: 0\n")
2243
2244            if details in ["full", "analytics"]:
2245                # -- Show analytics section:
2246                if view["stat"]["portfolioCostRUB"] > 0:
2247                    info.extend([
2248                        "\n# Analytics\n"
2249                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2250                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2251                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2252                            view["stat"]["totalChangesRUB"],
2253                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2254                            view["stat"]["totalChangesPercentRUB"],
2255                        ),
2256                        "\n## Portfolio distribution by assets\n"
2257                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2258                        "|------------|---------|---------|--------------------|\n",
2259                    ])
2260
2261                    for key in view["analytics"]["distrByAssets"].keys():
2262                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2263                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2264                                key,
2265                                view["analytics"]["distrByAssets"][key]["uniques"],
2266                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2267                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2268                            ))
2269
2270                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2271                    info.extend([
2272                        "\n## Portfolio distribution by companies\n"
2273                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2274                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2275                    ])
2276
2277                    for company in view["analytics"]["distrByCompanies"].keys():
2278                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2279                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2280                            info.append("| {} | {:<7} | {:<18} |\n".format(
2281                                "{}{}{}".format(
2282                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2283                                    company,
2284                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2285                                ),
2286                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2287                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2288                            ))
2289
2290                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2291                    info.extend([
2292                        "\n## Portfolio distribution by sectors\n"
2293                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2294                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2295                    ])
2296
2297                    for sector in view["analytics"]["distrBySectors"].keys():
2298                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2299                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2300                                sector,
2301                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2302                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2303                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2304                            ))
2305
2306                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2307                    info.extend([
2308                        "\n## Portfolio distribution by currencies\n"
2309                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2310                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2311                    ])
2312
2313                    for curr in view["analytics"]["distrByCurrencies"].keys():
2314                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2315                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2316                            info.append("| {} | {:<7} | {:<18} |\n".format(
2317                                "[{}] {}{}".format(
2318                                    curr,
2319                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2320                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2321                                ),
2322                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2323                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2324                            ))
2325
2326                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2327                    info.extend([
2328                        "\n## Portfolio distribution by countries\n"
2329                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2330                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2331                    ])
2332
2333                    for country in view["analytics"]["distrByCountries"].keys():
2334                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2335                            nameLen = len(country)
2336                            info.append("| {} | {:<7} | {:<18} |\n".format(
2337                                "{}{}".format(
2338                                    country,
2339                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2340                                ),
2341                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2342                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2343                            ))
2344
2345            infoText = "".join(info)
2346
2347            uLogger.info(infoText)
2348
2349            if details == "full" and self.overviewFile:
2350                filename = self.overviewFile
2351
2352            elif details == "digest" and self.overviewDigestFile:
2353                filename = self.overviewDigestFile
2354
2355            elif details == "positions" and self.overviewPositionsFile:
2356                filename = self.overviewPositionsFile
2357
2358            elif details == "orders" and self.overviewOrdersFile:
2359                filename = self.overviewOrdersFile
2360
2361            elif details == "analytics" and self.overviewAnalyticsFile:
2362                filename = self.overviewAnalyticsFile
2363
2364            else:
2365                filename = ""
2366
2367            if filename:
2368                with open(filename, "w", encoding="UTF-8") as fH:
2369                    fH.write(infoText)
2370
2371                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2372
2373        return view
2374
2375    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2376        """
2377        Returns history operations between two given dates for current `accountId`.
2378        If `reportFile` string is not empty then also save human-readable report.
2379        Shows some statistical data of closed positions.
2380
2381        :param start: see docstring in `GetDatesAsString()` method
2382        :param end: see docstring in `GetDatesAsString()` method
2383        :param show: if `True` then also prints all records to the console.
2384        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2385        :return: original list of dictionaries with history of deals records from API ("operations" key):
2386                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2387                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2388        """
2389        if self.accountId is None or not self.accountId:
2390            uLogger.error("Variable `accountId` must be defined for using this method!")
2391            raise Exception("Account ID required")
2392
2393        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2394
2395        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2396
2397        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2398        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2399        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2400        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2401        customStat = {}  # custom statistics in additional to responseJSON
2402
2403        # --- output report in human-readable format:
2404        if show or self.reportFile:
2405            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2406            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2407            nextDay = ""
2408
2409            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2410
2411            if len(ops) > 0:
2412                customStat = {
2413                    "opsCount": 0,  # total operations count
2414                    "buyCount": 0,  # buy operations
2415                    "sellCount": 0,  # sell operations
2416                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2417                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2418                    "payIn": {"rub": 0.},  # Deposit brokerage account
2419                    "payOut": {"rub": 0.},  # Withdrawals
2420                    "divs": {"rub": 0.},  # Dividends income
2421                    "coupons": {"rub": 0.},  # Coupon's income
2422                    "brokerCom": {"rub": 0.},  # Service commissions
2423                    "serviceCom": {"rub": 0.},  # Service commissions
2424                    "marginCom": {"rub": 0.},  # Margin commissions
2425                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2426                }
2427
2428                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2429                for item in ops:
2430                    if item["state"] == "OPERATION_STATE_EXECUTED":
2431                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2432
2433                        # count buy operations:
2434                        if "_BUY" in item["operationType"]:
2435                            customStat["buyCount"] += 1
2436
2437                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2438                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2439
2440                            else:
2441                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2442
2443                        # count sell operations:
2444                        elif "_SELL" in item["operationType"]:
2445                            customStat["sellCount"] += 1
2446
2447                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2448                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2449
2450                            else:
2451                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2452
2453                        # count incoming operations:
2454                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2455                            if item["payment"]["currency"] in customStat["payIn"].keys():
2456                                customStat["payIn"][item["payment"]["currency"]] += payment
2457
2458                            else:
2459                                customStat["payIn"][item["payment"]["currency"]] = payment
2460
2461                        # count withdrawals operations:
2462                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2463                            if item["payment"]["currency"] in customStat["payOut"].keys():
2464                                customStat["payOut"][item["payment"]["currency"]] += payment
2465
2466                            else:
2467                                customStat["payOut"][item["payment"]["currency"]] = payment
2468
2469                        # count dividends income:
2470                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2471                            if item["payment"]["currency"] in customStat["divs"].keys():
2472                                customStat["divs"][item["payment"]["currency"]] += payment
2473
2474                            else:
2475                                customStat["divs"][item["payment"]["currency"]] = payment
2476
2477                        # count coupon's income:
2478                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2479                            if item["payment"]["currency"] in customStat["coupons"].keys():
2480                                customStat["coupons"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["coupons"][item["payment"]["currency"]] = payment
2484
2485                        # count broker commissions:
2486                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2487                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2488                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2489
2490                            else:
2491                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2492
2493                        # count service commissions:
2494                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2495                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2496                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2497
2498                            else:
2499                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2500
2501                        # count margin commissions:
2502                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2503                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2504                                customStat["marginCom"][item["payment"]["currency"]] += payment
2505
2506                            else:
2507                                customStat["marginCom"][item["payment"]["currency"]] = payment
2508
2509                        # count withholding taxes:
2510                        elif "_TAX" in item["operationType"]:
2511                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2512                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2513
2514                            else:
2515                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2516
2517                        else:
2518                            continue
2519
2520                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2521
2522                # --- view "Actions" lines:
2523                info.extend([
2524                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2525                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2526                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2527                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2528                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2529                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2530                    ),
2531                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2532                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2533                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2534                    ),
2535                ])
2536
2537                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2538                for key in opsKeys:
2539                    if key == "rub":
2540                        continue
2541
2542                    info.extend([
2543                        "|                            |                               | {:<28} |                      |                        |\n".format(
2544                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2545                        ),
2546                        "|                            |                               | {:<28} |                      |                        |\n".format(
2547                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2548                        ),
2549                    ])
2550
2551                info.append(splitLine1)
2552
2553                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2554                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2555                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2556                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2557                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2558                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2559                    )
2560
2561                # --- view "Payments" lines:
2562                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2563                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2564
2565                for key in paymentsKeys:
2566                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2567
2568                info.append(splitLine1)
2569
2570                # --- view "Commissions and taxes" lines:
2571                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2572                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2573
2574                for key in comKeys:
2575                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2576
2577                info.append(splitLine1)
2578
2579                info.extend([
2580                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2581                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2582                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2583                ])
2584
2585            else:
2586                info.append("Broker returned no operations during this period\n")
2587
2588            # --- view "Operations" section:
2589            for item in ops:
2590                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2591                    continue
2592
2593                else:
2594                    self.figi = item["figi"] if item["figi"] else ""
2595                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2596                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2597
2598                    # group of deals during one day:
2599                    if nextDay and item["date"].split("T")[0] != nextDay:
2600                        info.append(splitLine2)
2601                        nextDay = ""
2602
2603                    else:
2604                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2605
2606                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2607                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2608                        self.figi if self.figi else "—",
2609                        instrument["ticker"] if instrument else "—",
2610                        instrument["type"] if instrument else "—",
2611                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2612                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2613                        TKS_OPERATION_STATES[item["state"]],
2614                        TKS_OPERATION_TYPES[item["operationType"]],
2615                    ))
2616
2617            infoText = "".join(info)
2618
2619            if show:
2620                uLogger.info(infoText)
2621
2622            if self.reportFile:
2623                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2624                    fH.write(infoText)
2625
2626                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2627
2628        return ops, customStat
2629
2630    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2631        """
2632        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2633
2634        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2635        Warning! Broker server used ISO UTC time by default.
2636
2637        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2638        Also, `historyFile` used to update history with `onlyMissing` parameter.
2639
2640        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2641
2642        :param start: see docstring in `GetDatesAsString()` method.
2643        :param end: see docstring in `GetDatesAsString()` method.
2644        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2645                         `"hour"`, `"day"`. Default: `"hour"`.
2646        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2647                            False by default. Warning! History appends only from last candle to current time
2648                            with always update last candle!
2649        :param csvSep: separator if csv-file is used, `,` by default.
2650        :param show: if `True` then also prints Pandas DataFrame to the console.
2651        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2652                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2653        """
2654        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2655        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2656        history = None  # empty pandas object for history
2657
2658        if interval not in TKS_CANDLE_INTERVALS.keys():
2659            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2660            raise Exception("Incorrect value")
2661
2662        if not (self.ticker or self.figi):
2663            uLogger.error("Ticker or FIGI must be defined!")
2664            raise Exception("Ticker or FIGI required")
2665
2666        if self.ticker and not self.figi:
2667            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2668            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2669
2670        if self.figi and not self.ticker:
2671            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2672            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2673
2674        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2675        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2676        if interval.lower() != "day":
2677            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2678
2679        delta = dtEnd - dtStart  # current UTC time minus last time in file
2680        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2681
2682        # calculate history length in candles:
2683        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2684        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2685            length += 1  # to avoid fraction time
2686
2687        # calculate data blocks count:
2688        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2689
2690        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2691        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2692        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2693        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2694        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2695
2696        tempOld = None  # pandas object for old history, if --only-missing key present
2697        lastTime = None  # datetime object of last old candle in file
2698
2699        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2700            uLogger.debug("--only-missing key present, add only last missing candles...")
2701            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2702
2703            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2704
2705            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2706            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2707            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2708            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2709
2710            # get last datetime object from last string in file or minus 1 delta if file is empty:
2711            if len(tempOld) > 0:
2712                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2713
2714            else:
2715                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2716
2717            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2718
2719        responseJSONs = []  # raw history blocks of data
2720
2721        blockEnd = dtEnd
2722        for item in range(blocks):
2723            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2724            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2725
2726            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2727                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2728            ))
2729
2730            if blockStart == blockEnd:
2731                uLogger.debug("Skipped this zero-length block...")
2732
2733            else:
2734                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2735                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2736                self.body = str({
2737                    "figi": self.figi,
2738                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2739                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2740                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2741                })
2742                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2743
2744                if "code" in responseJSON.keys():
2745                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2746
2747                else:
2748                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2749                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2750
2751                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2752
2753            blockEnd = blockStart
2754
2755        printCount = len(responseJSONs)  # candles to show in console
2756        if responseJSONs:
2757            tempHistory = pd.DataFrame(
2758                data={
2759                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2760                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2761                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2762                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2763                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2764                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2765                    "volume": [int(item["volume"]) for item in responseJSONs],
2766                },
2767                index=range(len(responseJSONs)),
2768                columns=["date", "time", "open", "high", "low", "close", "volume"],
2769            )
2770            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2771            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2772
2773            # append only newest candles to old history if --only-missing key present:
2774            if onlyMissing and tempOld is not None and lastTime is not None:
2775                index = 0  # find start index in tempHistory data:
2776
2777                for i, item in tempHistory.iterrows():
2778                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2779
2780                    if curTime == lastTime:
2781                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2782                        index = i
2783                        printCount = index + 1
2784                        break
2785
2786                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2787
2788            else:
2789                history = tempHistory  # if no `--only-missing` key then load full data from server
2790
2791            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2792
2793        if history is not None and not history.empty:
2794            if show:
2795                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2796                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2797                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2798                ))
2799
2800        else:
2801            uLogger.warning("Received an empty candles history!")
2802
2803        if self.historyFile is not None:
2804            if history is not None and not history.empty:
2805                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2806                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2807
2808            else:
2809                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2810
2811        else:
2812            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2813
2814        return history
2815
2816    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2817        """
2818        Load candles history from csv-file and return Pandas DataFrame object.
2819
2820        See also: `History()` and `ShowHistoryChart()` methods.
2821
2822        :param filePath: path to csv-file to open.
2823        """
2824        loadedHistory = None  # init candles data object
2825
2826        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2827
2828        if os.path.exists(filePath):
2829            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2830
2831            tfStr = self.priceModel.FormattedDelta(
2832                self.priceModel.timeframe,
2833                "{days} days {hours}h {minutes}m {seconds}s",
2834            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2835                self.priceModel.timeframe,
2836                "{hours}h {minutes}m {seconds}s",
2837            )
2838
2839            if loadedHistory is not None and not loadedHistory.empty:
2840                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2841                    len(loadedHistory),
2842                    tfStr,
2843                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2844                )
2845
2846            else:
2847                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2848
2849        else:
2850            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2851
2852        return loadedHistory
2853
2854    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2855        """
2856        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2857
2858        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2859        Default: `index.html` (both for interact and non-interact candlesticks chart).
2860
2861        See also: `History()` and `LoadHistory()` methods.
2862
2863        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2864        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2865                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2866                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2867                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2868        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2869                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2870        """
2871        if isinstance(candles, str):
2872            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2873            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2874
2875        elif isinstance(candles, pd.DataFrame):
2876            self.priceModel.prices = candles  # set candles chain from variable
2877            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2878
2879            if "datetime" not in candles.columns:
2880                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2881
2882        else:
2883            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2884            raise Exception("Incorrect value")
2885
2886        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2887
2888        if interact:
2889            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2890
2891            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2892
2893        else:
2894            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2895
2896            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2897
2898        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2899
2900    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2901        """
2902        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2903        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2904
2905        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2906
2907        :param operation: string "Buy" or "Sell".
2908        :param lots: volume, integer count of lots >= 1.
2909        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2910        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2911        :param expDate: string "Undefined" by default or local date in future,
2912                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2913        :return: JSON with response from broker server.
2914        """
2915        if self.accountId is None or not self.accountId:
2916            uLogger.error("Variable `accountId` must be defined for using this method!")
2917            raise Exception("Account ID required")
2918
2919        if operation is None or not operation or operation not in ("Buy", "Sell"):
2920            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2921            raise Exception("Incorrect value")
2922
2923        if lots is None or lots < 1:
2924            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2925            lots = 1
2926
2927        if tp is None or tp < 0:
2928            tp = 0
2929
2930        if sl is None or sl < 0:
2931            sl = 0
2932
2933        if expDate is None or not expDate:
2934            expDate = "Undefined"
2935
2936        if not (self.ticker or self.figi):
2937            uLogger.error("Ticker or FIGI must be defined!")
2938            raise Exception("Ticker or FIGI required")
2939
2940        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2941        self.ticker = instrument["ticker"]
2942        self.figi = instrument["figi"]
2943
2944        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2945
2946        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2947        self.body = str({
2948            "figi": self.figi,
2949            "quantity": str(lots),
2950            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2951            "accountId": str(self.accountId),
2952            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2953        })
2954        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2955
2956        if "orderId" in response.keys():
2957            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2958                operation, response["orderId"],
2959                self.ticker, self.figi, lots,
2960                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2961                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2962                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2963            ))
2964
2965        else:
2966            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2967
2968        if tp > 0:
2969            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2970
2971        if sl > 0:
2972            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2973
2974        return response
2975
2976    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2977        """
2978        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2979        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2980
2981        See also: `Order()` and `Trade()` docstrings.
2982
2983        :param lots: volume, integer count of lots >= 1.
2984        :param tp: float > 0, take profit price of stop-order.
2985        :param sl: float > 0, stop loss price of stop-order.
2986        :param expDate: it's a local date in future.
2987                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2988        :return: JSON with response from broker server.
2989        """
2990        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2991
2992    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2993        """
2994        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2995        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2996
2997        See also: `Order()` and `Trade()` docstrings.
2998
2999        :param lots: volume, integer count of lots >= 1.
3000        :param tp: float > 0, take profit price of stop-order.
3001        :param sl: float > 0, stop loss price of stop-order.
3002        :param expDate: it's a local date in the future.
3003                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3004        :return: JSON with response from broker server.
3005        """
3006        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3007
3008    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
3009        """
3010        Close position of given instruments.
3011
3012        :param tickers: tickers list of instruments that must be closed.
3013        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3014                         This avoids unnecessary downloading data from the server.
3015        """
3016        if not tickers:
3017            uLogger.info("Tickers list is empty, nothing to close.")
3018
3019        else:
3020            if portfolio is None or not portfolio:
3021                portfolio = self.Overview(show=False)
3022
3023            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3024            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
3025
3026            for ticker in tickers:
3027                if ticker not in allOpenedTickers:
3028                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
3029                    continue
3030
3031                # search open trade info about instrument by ticker:
3032                instrument = {}
3033                for iType in TKS_INSTRUMENTS:
3034                    if instrument:
3035                        break
3036
3037                    for item in portfolio["stat"][iType]:
3038                        if item["ticker"] == ticker:
3039                            instrument = item
3040                            break
3041
3042                if instrument:
3043                    self.ticker = ticker
3044                    self.figi = instrument["figi"]
3045
3046                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3047                        self.ticker,
3048                        self.figi,
3049                        int(instrument["volume"]),
3050                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3051                    ))
3052
3053                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3054
3055                    if tradeLots > 0:
3056                        if instrument["blocked"] > 0:
3057                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3058                                instrument["blocked"],
3059                                self.ticker,
3060                                tradeLots,
3061                            ))
3062
3063                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3064                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3065
3066                    else:
3067                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3068
3069    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3070        """
3071        Close all positions of given instruments with defined type.
3072
3073        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3074        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3075                         This avoids unnecessary downloading data from the server.
3076        """
3077        if iType not in TKS_INSTRUMENTS:
3078            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3079
3080        else:
3081            if portfolio is None or not portfolio:
3082                portfolio = self.Overview(show=False)
3083
3084            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3085            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3086
3087            if tickers and portfolio:
3088                self.CloseTrades(tickers, portfolio)
3089
3090            else:
3091                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3092
3093    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3094        """
3095        Universal method to create market or limit orders with all available parameters for current `accountId`.
3096        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3097
3098        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3099        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3100
3101        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3102        then broker immediately open market order as you can do simple --buy or --sell operations!
3103
3104        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3105        When current price will go up or down to target price value then broker opens a limit order.
3106        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3107
3108        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3109
3110        :param operation: string "Buy" or "Sell".
3111        :param orderType: string "Limit" or "Stop".
3112        :param lots: volume, integer count of lots >= 1.
3113        :param targetPrice: target price > 0. This is open trade price for limit order.
3114        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3115                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3116        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3117                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3118                         Stop loss order always executed by market price.
3119        :param expDate: string "Undefined" by default or local date in future.
3120                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3121                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3122                        A limit order has no expiration date, it lasts until the end of the trading day.
3123        :return: JSON with response from broker server.
3124        """
3125        if self.accountId is None or not self.accountId:
3126            uLogger.error("Variable `accountId` must be defined for using this method!")
3127            raise Exception("Account ID required")
3128
3129        if operation is None or not operation or operation not in ("Buy", "Sell"):
3130            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3131            raise Exception("Incorrect value")
3132
3133        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3134            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3135            raise Exception("Incorrect value")
3136
3137        if lots is None or lots < 1:
3138            uLogger.error("You must define trade volume > 0: integer count of lots!")
3139            raise Exception("Incorrect value")
3140
3141        if targetPrice is None or targetPrice <= 0:
3142            uLogger.error("Target price for limit-order must be greater than 0!")
3143            raise Exception("Incorrect value")
3144
3145        if limitPrice is None or limitPrice <= 0:
3146            limitPrice = targetPrice
3147
3148        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3149            stopType = "Limit"
3150
3151        if expDate is None or not expDate:
3152            expDate = "Undefined"
3153
3154        if not (self.ticker or self.figi):
3155            uLogger.error("Tocker or FIGI must be defined!")
3156            raise Exception("Ticker or FIGI required")
3157
3158        response = {}
3159        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3160        self.ticker = instrument["ticker"]
3161        self.figi = instrument["figi"]
3162
3163        if orderType == "Limit":
3164            uLogger.debug(
3165                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3166                    self.ticker, self.figi,
3167                    operation, lots, targetPrice, instrument["currency"],
3168                ))
3169
3170            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3171            self.body = str({
3172                "figi": self.figi,
3173                "quantity": str(lots),
3174                "price": FloatToNano(targetPrice),
3175                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3176                "accountId": str(self.accountId),
3177                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3178            })
3179            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3180
3181            if "orderId" in response.keys():
3182                uLogger.info(
3183                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3184                        response["orderId"],
3185                        self.ticker, self.figi,
3186                        operation, lots, targetPrice, instrument["currency"],
3187                    ))
3188
3189                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3190                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3191                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3192                            targetPrice, instrument["currency"],
3193                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3194                        ))
3195
3196                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3197                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3198                            targetPrice, instrument["currency"],
3199                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3200                        ))
3201
3202            else:
3203                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3204
3205        if orderType == "Stop":
3206            uLogger.debug(
3207                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3208                    self.ticker, self.figi,
3209                    operation, lots,
3210                    targetPrice, instrument["currency"],
3211                    limitPrice, instrument["currency"],
3212                    stopType, expDate,
3213                ))
3214
3215            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3216            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3217            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3218
3219            body = {
3220                "figi": self.figi,
3221                "quantity": str(lots),
3222                "price": FloatToNano(limitPrice),
3223                "stopPrice": FloatToNano(targetPrice),
3224                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3225                "accountId": str(self.accountId),
3226                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3227                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3228            }
3229
3230            if expDateUTC:
3231                body["expireDate"] = expDateUTC
3232
3233            self.body = str(body)
3234            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3235
3236            if "stopOrderId" in response.keys():
3237                uLogger.info(
3238                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3239                        response["stopOrderId"],
3240                        self.ticker, self.figi,
3241                        operation, lots,
3242                        targetPrice, instrument["currency"],
3243                        limitPrice, instrument["currency"],
3244                        TKS_STOP_ORDER_TYPES[stopOrderType],
3245                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3246                    ))
3247
3248                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3249                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3250                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3251                            targetPrice, instrument["currency"],
3252                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3253                        ))
3254
3255                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3256                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3257                            targetPrice, instrument["currency"],
3258                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3259                        ))
3260
3261            else:
3262                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3263
3264        return response
3265
3266    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3267        """
3268        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3269        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3270        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3271        See also: `Order()` docstring.
3272
3273        :param lots: volume, integer count of lots >= 1.
3274        :param targetPrice: target price > 0. This is open trade price for limit order.
3275        :return: JSON with response from broker server.
3276        """
3277        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3278
3279    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3280        """
3281        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3282        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3283        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3284        target price value then broker opens a limit order. See also: `Order()` docstring.
3285
3286        :param lots: volume, integer count of lots >= 1.
3287        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3288        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3289                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3290        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3291                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3292        :param expDate: string "Undefined" by default or local date in future.
3293                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3294                        This date is converting to UTC format for server.
3295        :return: JSON with response from broker server.
3296        """
3297        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3298
3299    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3300        """
3301        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3302        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3303        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3304        See also: `Order()` docstring.
3305
3306        :param lots: volume, integer count of lots >= 1.
3307        :param targetPrice: target price > 0. This is open trade price for limit order.
3308        :return: JSON with response from broker server.
3309        """
3310        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3311
3312    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3313        """
3314        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3315        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3316        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3317        target price value then broker opens a limit order. See also: `Order()` docstring.
3318
3319        :param lots: volume, integer count of lots >= 1.
3320        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3321        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3322                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3323        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3324                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3325        :param expDate: string "Undefined" by default or local date in future.
3326                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3327                        This date is converting to UTC format for server.
3328        :return: JSON with response from broker server.
3329        """
3330        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3331
3332    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3333        """
3334        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3335
3336        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3337        :param allOrdersIDs: pre-received lists of all active pending orders.
3338                             This avoids unnecessary downloading data from the server.
3339        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3340        """
3341        if self.accountId is None or not self.accountId:
3342            uLogger.error("Variable `accountId` must be defined for using this method!")
3343            raise Exception("Account ID required")
3344
3345        if orderIDs:
3346            if allOrdersIDs is None or not allOrdersIDs:
3347                rawOrders = self.RequestPendingOrders()
3348                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3349
3350            if allStopOrdersIDs is None or not allStopOrdersIDs:
3351                rawStopOrders = self.RequestStopOrders()
3352                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3353
3354            for orderID in orderIDs:
3355                idInPendingOrders = orderID in allOrdersIDs
3356                idInStopOrders = orderID in allStopOrdersIDs
3357
3358                if not (idInPendingOrders or idInStopOrders):
3359                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3360                    continue
3361
3362                else:
3363                    if idInPendingOrders:
3364                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3365
3366                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3367                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3368                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3369                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3370
3371                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3372                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3373                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3374
3375                        else:
3376                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3377
3378                    elif idInStopOrders:
3379                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3380
3381                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3382                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3383                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3384                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3385
3386                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3387                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3388                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3389
3390                        else:
3391                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3392
3393                    else:
3394                        continue
3395
3396    def CloseAllOrders(self) -> None:
3397        """
3398        Gets a list of open pending and stop orders and cancel it all.
3399        """
3400        rawOrders = self.RequestPendingOrders()
3401        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3402        lenOrders = len(allOrdersIDs)
3403
3404        rawStopOrders = self.RequestStopOrders()
3405        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3406        lenSOrders = len(allStopOrdersIDs)
3407
3408        if lenOrders > 0 or lenSOrders > 0:
3409            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3410
3411            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3412
3413        else:
3414            uLogger.info("Orders not found, nothing to cancel.")
3415
3416    def CloseAll(self, *args) -> None:
3417        """
3418        Close all available (not blocked) opened trades and orders.
3419
3420        Also, you can select one or more keywords case-insensitive:
3421        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3422
3423        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3424        """
3425        overview = self.Overview(show=False)  # get all open trades info
3426
3427        if len(args) == 0:
3428            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3429            self.CloseAllOrders()  # close all pending and stop orders
3430
3431            for iType in TKS_INSTRUMENTS:
3432                if iType != "Currencies":
3433                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3434
3435        else:
3436            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3437            lowerArgs = [x.lower() for x in args]
3438
3439            if "orders" in lowerArgs:
3440                self.CloseAllOrders()  # close all pending and stop orders
3441
3442            for iType in TKS_INSTRUMENTS:
3443                if iType.lower() in lowerArgs and iType != "Currencies":
3444                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3445
3446    @staticmethod
3447    def ParseOrderParameters(operation, **inputParameters):
3448        """
3449        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3450
3451        :param operation: string "Buy" or "Sell".
3452        :param inputParameters: this is dict of strings that looks like this
3453               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3454               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3455               "prices" key: one or more prices to open limit-orders
3456               Counts of values in lots and prices lists must be equals!
3457        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3458        """
3459        # TODO: update order grid work with api v2
3460        pass
3461        # uLogger.debug("Input parameters: {}".format(inputParameters))
3462        #
3463        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3464        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3465        #     raise Exception("Incorrect value")
3466        #
3467        # if "l" in inputParameters.keys():
3468        #     inputParameters["lots"] = inputParameters.pop("l")
3469        #
3470        # if "p" in inputParameters.keys():
3471        #     inputParameters["prices"] = inputParameters.pop("p")
3472        #
3473        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3474        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3475        #     raise Exception("Incorrect value")
3476        #
3477        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3478        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3479        #
3480        # if len(lots) != len(prices):
3481        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3482        #     raise Exception("Incorrect value")
3483        #
3484        # uLogger.debug("Extracted parameters for orders:")
3485        # uLogger.debug("lots = {}".format(lots))
3486        # uLogger.debug("prices = {}".format(prices))
3487        #
3488        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3489        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3490        # uLogger.debug("Order parameters: {}".format(result))
3491        #
3492        # return result
3493
3494    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3495        """
3496        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3497
3498        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3499        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3500        """
3501        result = False
3502        msg = "Instrument not defined!"
3503
3504        if portfolio is None or not portfolio:
3505            portfolio = self.Overview(show=False)
3506
3507        if self.ticker:
3508            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3509            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3510
3511            for iType in TKS_INSTRUMENTS:
3512                for instrument in portfolio["stat"][iType]:
3513                    if instrument["ticker"] == self.ticker:
3514                        result = True
3515                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3516                        break
3517
3518        elif self.figi:
3519            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3520            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3521
3522            for iType in TKS_INSTRUMENTS:
3523                for instrument in portfolio["stat"][iType]:
3524                    if instrument["figi"] == self.figi:
3525                        result = True
3526                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3527                        break
3528
3529        else:
3530            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3531
3532        uLogger.debug(msg)
3533
3534        return result
3535
3536    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3537        """
3538        Returns instrument is in the user's portfolio if it presents there.
3539        Instrument must be defined by `ticker` (highly priority) or `figi`.
3540
3541        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3542        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3543        """
3544        result = None
3545        msg = "Instrument not defined!"
3546
3547        if portfolio is None or not portfolio:
3548            portfolio = self.Overview(show=False)
3549
3550        if self.ticker:
3551            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3552            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3553
3554            for iType in TKS_INSTRUMENTS:
3555                for instrument in portfolio["stat"][iType]:
3556                    if instrument["ticker"] == self.ticker:
3557                        result = instrument
3558                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3559                        break
3560
3561        elif self.figi:
3562            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3563            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3564
3565            for iType in TKS_INSTRUMENTS:
3566                for instrument in portfolio["stat"][iType]:
3567                    if instrument["figi"] == self.figi:
3568                        result = instrument
3569                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3570                        break
3571
3572        else:
3573            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3574
3575        uLogger.debug(msg)
3576
3577        return result
3578
3579    def RequestLimits(self) -> dict:
3580        """
3581        Method for obtaining the available funds for withdrawal for current `accountId`.
3582
3583        See also:
3584        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3585        - `OverviewLimits()` method
3586
3587        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3588                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3589                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3590                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3591        """
3592        if self.accountId is None or not self.accountId:
3593            uLogger.error("Variable `accountId` must be defined for using this method!")
3594            raise Exception("Account ID required")
3595
3596        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3597
3598        self.body = str({"accountId": self.accountId})
3599        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3600        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3601
3602        uLogger.debug("Records about available funds for withdrawal successfully received")
3603
3604        return rawLimits
3605
3606    def OverviewLimits(self, show: bool = False) -> dict:
3607        """
3608        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3609
3610        See also: `RequestLimits()`.
3611
3612        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3613        :return: dict with raw parsed data from server and some calculated statistics about it.
3614        """
3615        if self.accountId is None or not self.accountId:
3616            uLogger.error("Variable `accountId` must be defined for using this method!")
3617            raise Exception("Account ID required")
3618
3619        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3620
3621        view = {
3622            "rawLimits": rawLimits,
3623            "limits": {  # parsed data for every currency:
3624                "money": {  # this is an array of portfolio currency positions
3625                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3626                },
3627                "blocked": {  # this is an array of blocked currency
3628                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3629                },
3630                "blockedGuarantee": {  # this is locked money under collateral for futures
3631                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3632                },
3633            },
3634        }
3635
3636        # --- Prepare text table with limits in human-readable format:
3637        if show:
3638            info = [
3639                "# Withdrawal limits\n\n",
3640                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3641                "* **Account ID:** [{}]\n".format(self.accountId),
3642                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3643                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3644            ]
3645
3646            for curr in view["limits"]["money"].keys():
3647                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3648                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3649                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3650
3651                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3652                    "[{}]".format(curr),
3653                    "{:.2f}".format(view["limits"]["money"][curr]),
3654                    "{:.2f}".format(availableMoney),
3655                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3656                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3657                )
3658
3659                if curr == "rub":
3660                    info.insert(5, infoStr)  # insert at first position in table and after headers
3661
3662                else:
3663                    info.append(infoStr)
3664
3665            infoText = "".join(info)
3666
3667            uLogger.info(infoText)
3668
3669            if self.withdrawalLimitsFile:
3670                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3671                    fH.write(infoText)
3672
3673                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3674
3675        return view
3676
3677    def RequestAccounts(self) -> dict:
3678        """
3679        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3680
3681        See also:
3682        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3683        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3684        - `OverviewUserInfo()` method
3685
3686        :return: dict with raw data from server that contains accounts info. Example of dict:
3687                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3688                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3689                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3690                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3691        """
3692        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3693
3694        self.body = str({})
3695        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3696        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3697
3698        uLogger.debug("Records about available accounts successfully received")
3699
3700        return rawAccounts
3701
3702    def RequestUserInfo(self) -> dict:
3703        """
3704        Method for requesting common user's information.
3705
3706        See also:
3707        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3708        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3709        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3710        - `OverviewUserInfo()` method
3711
3712        :return: dict with raw data from server that contains user's information. Example of dict:
3713                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3714                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3715        """
3716        uLogger.debug("Requesting common user's information. Wait, please...")
3717
3718        self.body = str({})
3719        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3720        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3721
3722        uLogger.debug("Records about current user successfully received")
3723
3724        return rawUserInfo
3725
3726    def RequestMarginStatus(self, accountId: str = None) -> dict:
3727        """
3728        Method for requesting margin calculation for defined account ID.
3729
3730        See also:
3731        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3732        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3733        - `OverviewUserInfo()` method
3734
3735        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3736        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3737                 Example of responses:
3738                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3739                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3740                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3741                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3742                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3743                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3744        """
3745        if accountId is None or not accountId:
3746            if self.accountId is None or not self.accountId:
3747                uLogger.error("Variable `accountId` must be defined for using this method!")
3748                raise Exception("Account ID required")
3749
3750            else:
3751                accountId = self.accountId  # use `self.accountId` (main ID) by default
3752
3753        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3754
3755        self.body = str({"accountId": accountId})
3756        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3757        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3758
3759        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3760            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3761            rawMargin = {}
3762
3763        else:
3764            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3765
3766        return rawMargin
3767
3768    def RequestTariffLimits(self) -> dict:
3769        """
3770        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3771
3772        See also:
3773        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3774        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3775        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3776        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3777        - `OverviewUserInfo()` method
3778
3779        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3780                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3781                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3782        """
3783        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3784
3785        self.body = str({})
3786        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3787        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3788
3789        uLogger.debug("Records with limits of current tariff successfully received")
3790
3791        return rawTariffLimits
3792
3793    def RequestBondCoupons(self, iJSON: dict) -> dict:
3794        """
3795        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3796        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3797        All dates are in UTC timezone.
3798
3799        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3800        Documentation:
3801        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3802        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3803
3804        See also: `ExtendBondsData()`.
3805
3806        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3807                      If raw iJSON is not data of bond then server returns an error [400] with message:
3808                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3809        :return: dictionary with bond payment calendar. Response example
3810                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3811                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3812                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3813                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3814        """
3815        if iJSON["figi"] is None or not iJSON["figi"]:
3816            uLogger.error("FIGI must be defined for using this method!")
3817            raise Exception("FIGI required")
3818
3819        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3820        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3821
3822        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3823            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3824            self.figi,
3825            startDate,
3826            endDate,
3827        ))
3828
3829        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3830        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3831        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3832
3833        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3834            uLogger.warning("Instrument type is not bond!")
3835
3836        else:
3837            uLogger.debug("Records about bond payment calendar successfully received")
3838
3839        return calendar
3840
3841    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3842        """
3843        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3844        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3845        coupon yields, current yields and some statistics etc.
3846
3847        WARNING! This is too long operation if a lot of bonds requested from broker server.
3848
3849        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3850
3851        :param instruments: list of strings with tickers or FIGIs.
3852        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3853                     for further used by data scientists or stock analytics.
3854        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3855                 In XLSX-file and Pandas DataFrame fields mean:
3856                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3857                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3858        """
3859        if instruments is None or not instruments:
3860            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3861            raise Exception("Ticker or FIGI required")
3862
3863        if isinstance(instruments, str):
3864            instruments = [instruments]
3865
3866        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3867
3868        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3869
3870        iCount = len(uniqueInstruments)
3871        tooLong = iCount >= 20
3872        if tooLong:
3873            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3874
3875        bonds = None
3876        for i, self.figi in enumerate(uniqueInstruments):
3877            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3878
3879            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3880                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3881                rawBond = self.SearchByFIGI(requestPrice=True)
3882
3883                # Widen raw data with UTC current time (iData["actualDateTime"]):
3884                actualDate = datetime.now(tzutc())
3885                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3886
3887                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3888                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3889
3890                # Replace some values with human-readable:
3891                iData["nominalCurrency"] = iData["nominal"]["currency"]
3892                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3893                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3894                iData["aciCurrency"] = iData["aciValue"]["currency"]
3895                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3896                iData["issueSize"] = int(iData["issueSize"])
3897                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3898                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3899                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3900                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3901                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3902                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3903                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3904                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3905                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3906                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3907
3908                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3909                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3910                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3911                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3912                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3913                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3914                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3915                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3916                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3917                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3918                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3919
3920                # Widen raw data with calendar data from `rawCalendar` values:
3921                calendarData = []
3922                for item in iData["rawCalendar"]["events"]:
3923                    calendarData.append({
3924                        "couponDate": item["couponDate"],
3925                        "couponNumber": int(item["couponNumber"]),
3926                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3927                        "payCurrency": item["payOneBond"]["currency"],
3928                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3929                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3930                        "couponStartDate": item["couponStartDate"],
3931                        "couponEndDate": item["couponEndDate"],
3932                        "couponPeriod": item["couponPeriod"],
3933                    })
3934
3935                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3936                if "maturityDate" not in iData.keys():
3937                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3938
3939                # Widen raw data with Coupon Rate.
3940                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3941                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3942                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3943                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3944
3945                # Widen raw data with Yield to Maturity (YTM) on current date.
3946                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3947                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3948                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3949                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3950                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3951                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3952
3953                iData["calendar"] = calendarData  # adds calendar at the end
3954
3955                # Remove not used data:
3956                iData.pop("uid")
3957                iData.pop("positionUid")
3958                iData.pop("currentPrice")
3959                iData.pop("rawCalendar")
3960
3961                colNames = list(iData.keys())
3962                if bonds is None:
3963                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3964
3965                else:
3966                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3967
3968            else:
3969                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3970
3971            processed = round(100 * (i + 1) / iCount, 1)
3972            if tooLong and processed % 5 == 0:
3973                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3974
3975            else:
3976                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3977
3978        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3979
3980        # Saving bonds from Pandas DataFrame to XLSX sheet:
3981        if xlsx and self.bondsXLSXFile:
3982            with pd.ExcelWriter(
3983                    path=self.bondsXLSXFile,
3984                    date_format=TKS_DATE_FORMAT,
3985                    datetime_format=TKS_DATE_TIME_FORMAT,
3986                    mode="w",
3987            ) as writer:
3988                bonds.to_excel(
3989                    writer,
3990                    sheet_name="Extended bonds data",
3991                    index=True,
3992                    encoding="UTF-8",
3993                    freeze_panes=(1, 1),
3994                )  # saving as XLSX-file with freeze first row and column as headers
3995
3996            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3997
3998        return bonds
3999
4000    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4001        """
4002        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4003
4004        WARNING! This is too long operation if a lot of bonds requested from broker server.
4005
4006        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4007
4008        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4009                        extended information about bonds: main info, current prices, bond payment calendar,
4010                        coupon yields, current yields and some statistics etc.
4011                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4012        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4013                     for further used by data scientists or stock analytics.
4014        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4015        """
4016        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4017            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4018
4019        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4020
4021        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4022        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4023        calendar = None
4024        for bond in extBonds.iterrows():
4025            for item in bond[1]["calendar"]:
4026                cData = {
4027                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4028                    "couponDate": item["couponDate"],
4029                    "figi": bond[1]["figi"],
4030                    "ticker": bond[1]["ticker"],
4031                    "name": bond[1]["name"],
4032                    "couponNumber": item["couponNumber"],
4033                    "payOneBond": item["payOneBond"],
4034                    "payCurrency": item["payCurrency"],
4035                    "couponType": item["couponType"],
4036                    "couponPeriod": item["couponPeriod"],
4037                    "fixDate": item["fixDate"],
4038                    "couponStartDate": item["couponStartDate"],
4039                    "couponEndDate": item["couponEndDate"],
4040                }
4041
4042                if calendar is None:
4043                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4044
4045                else:
4046                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4047
4048        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4049
4050        # Saving calendar from Pandas DataFrame to XLSX sheet:
4051        if xlsx:
4052            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4053
4054            with pd.ExcelWriter(
4055                    path=xlsxCalendarFile,
4056                    date_format=TKS_DATE_FORMAT,
4057                    datetime_format=TKS_DATE_TIME_FORMAT,
4058                    mode="w",
4059            ) as writer:
4060                humanReadable = calendar.copy(deep=True)
4061                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4062                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4063                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4064                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4065                humanReadable.columns = colNames  # human-readable column names
4066
4067                humanReadable.to_excel(
4068                    writer,
4069                    sheet_name="Bond payments calendar",
4070                    index=False,
4071                    encoding="UTF-8",
4072                    freeze_panes=(1, 2),
4073                )  # saving as XLSX-file with freeze first row and column as headers
4074
4075                del humanReadable  # release df in memory
4076
4077            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4078
4079        return calendar
4080
4081    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4082        """
4083        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4084        Also, creates Markdown file with calendar data, `calendar.md` by default.
4085
4086        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4087
4088        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4089                        extended information about bonds: main info, current prices, bond payment calendar,
4090                        coupon yields, current yields and some statistics etc.
4091                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4092        :param show: if `True` then also printing bonds payment calendar to the console,
4093                     otherwise save to file `calendarFile` only. `False` by default.
4094        :return: multilines text in Markdown format with bonds payment calendar as a table.
4095        """
4096        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4097            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4098
4099        infoText = "# Bond payments calendar\n\n"
4100
4101        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4102
4103        if not calendar.empty:
4104            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4105
4106            info = [
4107                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4108                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4109            ]
4110
4111            newMonth = False
4112            notOneBond = calendar["figi"].nunique() > 1
4113            for i, bond in enumerate(calendar.iterrows()):
4114                if newMonth and notOneBond:
4115                    info.append(splitLine)
4116
4117                info.append(
4118                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4119                        "  √" if bond[1]["paid"] else "  —",
4120                        bond[1]["couponDate"].split("T")[0],
4121                        bond[1]["figi"],
4122                        bond[1]["ticker"],
4123                        bond[1]["couponNumber"],
4124                        "{} {}".format(
4125                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4126                            bond[1]["payCurrency"],
4127                        ),
4128                        bond[1]["couponType"],
4129                        bond[1]["couponPeriod"],
4130                        bond[1]["fixDate"].split("T")[0],
4131                    )
4132                )
4133
4134                if i < len(calendar.values) - 1:
4135                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4136                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4137                    newMonth = False if curDate.month == nextDate.month else True
4138
4139                else:
4140                    newMonth = False
4141
4142            infoText += "".join(info)
4143
4144            if show:
4145                uLogger.info("{}".format(infoText))
4146
4147            if self.calendarFile is not None:
4148                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4149                    fH.write(infoText)
4150
4151                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4152
4153        else:
4154            infoText += "No data\n"
4155
4156        return infoText
4157
4158    def OverviewAccounts(self, show: bool = False) -> dict:
4159        """
4160        Method for parsing and show simple table with all available user accounts.
4161
4162        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4163
4164        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4165        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4166                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4167                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4168                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4169                                                        "closed": "—", "access": "Full access" }, ...}}`
4170        """
4171        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4172
4173        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4174        accounts = {
4175            item["id"]: {
4176                "type": TKS_ACCOUNT_TYPES[item["type"]],
4177                "name": item["name"],
4178                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4179                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4180                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4181                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4182            } for item in rawAccounts["accounts"]
4183        }
4184
4185        # Raw and parsed data with some fields replaced in "stat" section:
4186        view = {
4187            "rawAccounts": rawAccounts,
4188            "stat": accounts,
4189        }
4190
4191        # --- Prepare simple text table with only accounts data in human-readable format:
4192        if show:
4193            info = [
4194                "# User accounts\n\n",
4195                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4196                "| Account ID   | Type                      | Status                    | Name                           |\n",
4197                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4198            ]
4199
4200            for account in view["stat"].keys():
4201                info.extend([
4202                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4203                        account,
4204                        view["stat"][account]["type"],
4205                        view["stat"][account]["status"],
4206                        view["stat"][account]["name"],
4207                    )
4208                ])
4209
4210            infoText = "".join(info)
4211
4212            uLogger.info(infoText)
4213
4214            if self.userAccountsFile:
4215                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4216                    fH.write(infoText)
4217
4218                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4219
4220        return view
4221
4222    def OverviewUserInfo(self, show: bool = False) -> dict:
4223        """
4224        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4225
4226        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4227
4228        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4229        :return: dict with raw parsed data from server and some calculated statistics about it.
4230        """
4231        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4232        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4233        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4234        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4235        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4236        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4237
4238        # This is dict with parsed common user data:
4239        userInfo = {
4240            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4241            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4242            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4243            "tariff": rawUserInfo["tariff"],
4244        }
4245
4246        # This is an array of dict with parsed margin statuses for every account IDs:
4247        margins = {}
4248        for accountId in accounts.keys():
4249            if rawMargins[accountId]:
4250                margins[accountId] = {
4251                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4252                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4253                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4254                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4255                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4256                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4257                }
4258
4259            else:
4260                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4261
4262        unary = {}  # unary-connection limits
4263        for item in rawTariffLimits["unaryLimits"]:
4264            if item["limitPerMinute"] in unary.keys():
4265                unary[item["limitPerMinute"]].extend(item["methods"])
4266
4267            else:
4268                unary[item["limitPerMinute"]] = item["methods"]
4269
4270        stream = {}  # stream-connection limits
4271        for item in rawTariffLimits["streamLimits"]:
4272            if item["limit"] in stream.keys():
4273                stream[item["limit"]].extend(item["streams"])
4274
4275            else:
4276                stream[item["limit"]] = item["streams"]
4277
4278        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4279        limits = {
4280            "unary": unary,
4281            "stream": stream,
4282        }
4283
4284        # Raw and parsed data as an output result:
4285        view = {
4286            "rawUserInfo": rawUserInfo,
4287            "rawAccounts": rawAccounts,
4288            "rawMargins": rawMargins,
4289            "rawTariffLimits": rawTariffLimits,
4290            "stat": {
4291                "userInfo": userInfo,
4292                "accounts": accounts,
4293                "margins": margins,
4294                "limits": limits,
4295            },
4296        }
4297
4298        # --- Prepare text table with user information in human-readable format:
4299        if show:
4300            info = [
4301                "# Full user information\n\n",
4302                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4303                "## Common information\n\n",
4304                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4305                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4306                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4307                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4308                "\n## User accounts\n\n",
4309            ]
4310
4311            for account in view["stat"]["accounts"].keys():
4312                info.extend([
4313                    "### ID: [{}]\n\n".format(account),
4314                    "| Parameters           | Values                                                       |\n",
4315                    "|----------------------|--------------------------------------------------------------|\n",
4316                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4317                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4318                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4319                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4320                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4321                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4322                ])
4323
4324                if margins[account]:
4325                    info.extend([
4326                        "| Margin status:       | Enabled                                                      |\n",
4327                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4328                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4329                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4330                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4331                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4332                    ])
4333
4334                else:
4335                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4336
4337            info.extend([
4338                "\n## Current user tariff limits\n",
4339                "\nSee also:\n",
4340                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4341                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4342                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4343                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4344                "\n### Unary limits\n",
4345            ])
4346
4347            if unary:
4348                for key, values in sorted(unary.items()):
4349                    info.append("\n* Max requests per minute: {}\n".format(key))
4350
4351                    for value in values:
4352                        info.append("  - {}\n".format(value))
4353
4354            else:
4355                info.append("\nNot available\n")
4356
4357            info.append("\n### Stream limits\n")
4358
4359            if stream:
4360                for key, values in sorted(stream.items()):
4361                    info.append("\n* Max stream connections: {}\n".format(key))
4362
4363                    for value in values:
4364                        info.append("  - {}\n".format(value))
4365
4366            else:
4367                info.append("\nNot available\n")
4368
4369            infoText = "".join(info)
4370
4371            uLogger.info(infoText)
4372
4373            if self.userInfoFile:
4374                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4375                    fH.write(infoText)
4376
4377                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4378
4379        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
199        """
200        Main class init.
201
202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
205        :param useCache: use default cache file with raw data to use instead of `iList`.
206                         True by default. Cache is auto-update if new day has come.
207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
208        :param defaultCache: path to default cache file. `dump.json` by default.
209        """
210        if token is None or not token:
211            try:
212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
214
215            except KeyError:
216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
217                raise Exception("Token required")
218
219        else:
220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
222
223        if accountId is None or not accountId:
224            try:
225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
227
228            except KeyError:
229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
230
231        else:
232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
234
235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
237
238        Latest version: https://pypi.org/project/tksbrokerapi/
239        """
240
241        self.aliases = TKS_TICKER_ALIASES
242        """Some aliases instead official tickers.
243
244        See also: `TKSEnums.TKS_TICKER_ALIASES`
245        """
246
247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
248
249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
250
251        self.ticker = ""
252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
253
254        See also: `SearchByTicker()`, `SearchInstruments()`.
255        """
256
257        self.figi = ""
258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
259
260        See also: `SearchByFIGI()`, `SearchInstruments()`.
261        """
262
263        self.depth = 1
264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
265
266        See also: `GetCurrentPrices()`.
267        """
268
269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
271
272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
273        """
274
275        uLogger.debug("Broker API server: {}".format(self.server))
276
277        self.timeout = 15
278        """Server operations timeout in seconds. Default: `15`.
279
280        See also: `SendAPIRequest()`.
281        """
282
283        self.headers = {
284            "Content-Type": "application/json",
285            "accept": "application/json",
286            "Authorization": "Bearer {}".format(self.token),
287            "x-app-name": "Tim55667757.TKSBrokerAPI",
288        }
289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
290
291        See also: `SendAPIRequest()`.
292        """
293
294        self.body = None
295        """Request body which send to broker server. Default: `None`.
296
297        See also: `SendAPIRequest()`.
298        """
299
300        self.historyFile = None
301        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
302
303        See also: `History()`.
304        """
305
306        self.htmlHistoryFile = "index.html"
307        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
308
309        See also: `ShowHistoryChart()`.
310        """
311
312        self.instrumentsFile = "instruments.md"
313        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
314
315        See also: `ShowInstrumentsInfo()`.
316        """
317
318        self.searchResultsFile = "search-results.md"
319        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
320
321        See also: `SearchInstruments()`.
322        """
323
324        self.pricesFile = "prices.md"
325        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
326
327        See also: `GetListOfPrices()`.
328        """
329
330        self.infoFile = "info.md"
331        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
332
333        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
334        """
335
336        self.bondsXLSXFile = "ext-bonds.xlsx"
337        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
338        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
339
340        See also: `ExtendBondsData()`.
341        """
342
343        self.calendarFile = "calendar.md"
344        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
345        
346        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
347
348        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
349        """
350
351        self.overviewFile = "overview.md"
352        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
353
354        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
355        """
356
357        self.overviewDigestFile = "overview-digest.md"
358        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
359
360        See also: `Overview()` with parameter `details="digest"`.
361        """
362
363        self.overviewPositionsFile = "overview-positions.md"
364        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
365
366        See also: `Overview()` with parameter `details="positions"`.
367        """
368
369        self.overviewOrdersFile = "overview-orders.md"
370        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
371
372        See also: `Overview()` with parameter `details="orders"`.
373        """
374
375        self.overviewAnalyticsFile = "overview-analytics.md"
376        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
377
378        See also: `Overview()` with parameter `details="analytics"`.
379        """
380
381        self.reportFile = "deals.md"
382        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
383
384        See also: `Deals()`.
385        """
386
387        self.withdrawalLimitsFile = "limits.md"
388        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
389
390        See also: `OverviewLimits()` and `RequestLimits()`.
391        """
392
393        self.userInfoFile = "user-info.md"
394        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
395
396        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
397        """
398
399        self.userAccountsFile = "accounts.md"
400        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
401
402        See also: `OverviewAccounts()`, `RequestAccounts()`.
403        """
404
405        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
406        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
407
408        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
409
410        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
411        """
412
413        self.iList = None  # init iList for raw instruments data
414        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
415        
416        See also: `Listing()`, `DumpInstruments()`.
417        """
418
419        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
420        if useCache:
421            if os.path.exists(self.iListDumpFile):
422                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
423                curTime = datetime.now(tzutc())
424
425                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
426                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
427
428                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
429
430                else:
431                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
432
433                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
434                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
435
436            else:
437                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
438                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
439
440        else:
441            self.iList = self.Listing()  # request new raw instruments data from broker server
442            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
443
444        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
445        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
446
447        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
448        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
472    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
473        """
474        Send GET or POST request to broker server and receive JSON object.
475
476        self.header: must be defining with dictionary of headers.
477        self.body: if define then used as request body. None by default.
478        self.timeout: global request timeout, 15 seconds by default.
479        :param url: url with REST request.
480        :param reqType: send "GET" or "POST" request. "GET" by default.
481        :param retry: how many times retry after first request if an 5xx server errors occurred.
482        :param pause: sleep time in seconds between retries.
483        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
484        :return: response JSON (dictionary) from broker.
485        """
486        if reqType not in ("GET", "POST"):
487            uLogger.error("You can define request type: 'GET' or 'POST'!")
488            raise Exception("Incorrect value")
489
490        if debug:
491            uLogger.debug("Request parameters:")
492            uLogger.debug("    - REST API URL: {}".format(url))
493            uLogger.debug("    - request type: {}".format(reqType))
494            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
495            uLogger.debug("    - body: {}".format(self.body))
496
497        # fast hack to avoid all operations with some tickers/FIGI
498        responseJSON = {}
499        oK = True
500        for item in self.exclude:
501            if item in url:
502                if debug:
503                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
504
505                oK = False
506                break
507
508        if oK:
509            counter = 0
510            response = None
511            errMsg = ""
512
513            while not response and counter <= retry:
514                if reqType == "GET":
515                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
516
517                if reqType == "POST":
518                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
519
520                if debug:
521                    uLogger.debug("Response:")
522                    uLogger.debug("    - status code: {}".format(response.status_code))
523                    uLogger.debug("    - reason: {}".format(response.reason))
524                    uLogger.debug("    - body length: {}".format(len(response.text)))
525                    uLogger.debug("    - headers: {}".format(response.headers))
526
527                # Server returns some headers:
528                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
529                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
530                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
531                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
532                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
533                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
534                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
535                    sleep(rateLimitWait)
536
537                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
538                if 400 <= response.status_code < 500:
539                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
540                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
541                    counter = retry + 1
542
543                if 500 <= response.status_code < 600:
544                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
545                    uLogger.debug("    - not oK, {}".format(errMsg))
546                    counter += 1
547
548                    if counter <= retry:
549                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
550                        sleep(pause)
551
552            responseJSON = self._ParseJSON(response.text)
553
554            if errMsg:
555                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
556                uLogger.error("    - not oK, {}".format(errMsg))
557
558        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
  • debug: if True then print more debug information, e.g. request and response parameters, headers etc.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
591    def Listing(self) -> dict:
592        """
593        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
594
595        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
596        """
597        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
598        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
599
600        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
601        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
602        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
603
604        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
605        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
606        poolUpdater.close()
607
608        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
609        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
610        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
611
612        # calculate minimum price increment (step) for all instruments and set up instrument's type:
613        for iType in iList.keys():
614            for ticker in iList[iType]:
615                iList[iType][ticker]["type"] = iType
616
617                if "minPriceIncrement" in iList[iType][ticker].keys():
618                    iList[iType][ticker]["step"] = NanoToFloat(
619                        iList[iType][ticker]["minPriceIncrement"]["units"],
620                        iList[iType][ticker]["minPriceIncrement"]["nano"],
621                    )
622
623                else:
624                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
625
626        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
628    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
629        """
630        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
631
632        See also: `DumpInstruments()`, `Listing()`.
633
634        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
635                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
636        """
637        if self.iListDumpFile is None or not self.iListDumpFile:
638            uLogger.error("Output name of dump file must be defined!")
639            raise Exception("Filename required")
640
641        if not self.iList or forceUpdate:
642            self.iList = self.Listing()
643
644        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
645
646        # Save as XLSX with separated sheets for every type of instruments:
647        with pd.ExcelWriter(
648                path=xlsxDumpFile,
649                date_format=TKS_DATE_FORMAT,
650                datetime_format=TKS_DATE_TIME_FORMAT,
651                mode="w",
652        ) as writer:
653            for iType in TKS_INSTRUMENTS:
654                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
655                df = df[sorted(df)]  # sorted by column names
656                df = df.applymap(
657                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
658                    na_action="ignore",
659                )  # converting numbers from nano-type to float in every cell
660                df.to_excel(
661                    writer,
662                    sheet_name=iType,
663                    encoding="UTF-8",
664                    freeze_panes=(1, 1),
665                )  # saving as XLSX-file with freeze first row and column as headers
666
667        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
669    def DumpInstruments(self, forceUpdate: bool = True) -> str:
670        """
671        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
672        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
673
674        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
675
676        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
677                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
678        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
679        """
680        if self.iListDumpFile is None or not self.iListDumpFile:
681            uLogger.error("Output name of dump file must be defined!")
682            raise Exception("Filename required")
683
684        if not self.iList or forceUpdate:
685            self.iList = self.Listing()
686
687        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
688        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
689            fH.write(jsonDump)
690
691        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
692
693        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
695    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
696        """
697        Show information about one instrument defined by json data and prints it in Markdown format.
698
699        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
700
701        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
702        :param show: if `True` then also printing information about instrument and its current price.
703        :return: multilines text in Markdown format with information about one instrument.
704        """
705        splitLine = "|                                                             |                                                        |\n"
706        infoText = ""
707
708        if iJSON is not None and iJSON and isinstance(iJSON, dict):
709            info = [
710                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
711                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
712                "| Parameters                                                  | Values                                                 |\n",
713                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
714                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
715                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
716            ]
717
718            if "sector" in iJSON.keys() and iJSON["sector"]:
719                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
720
721            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
722                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
723                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
724            )))
725
726            info.extend([
727                splitLine,
728                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
729                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
730            ])
731
732            if "isin" in iJSON.keys() and iJSON["isin"]:
733                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
734
735            if "classCode" in iJSON.keys():
736                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
737
738            info.extend([
739                splitLine,
740                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
741                splitLine,
742                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
743                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
744                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
745            ])
746
747            if iJSON["figi"]:
748                self.figi = iJSON["figi"]
749                iJSON = iJSON | self.RequestTradingStatus()
750
751                info.extend([
752                    splitLine,
753                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
754                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
755                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
756                ])
757
758            info.append(splitLine)
759
760            if "type" in iJSON.keys() and iJSON["type"]:
761                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
762
763            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
764                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
765
766            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
767                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
768
769            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
770                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
771
772            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
773                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
774
775            if "focusType" in iJSON.keys() and iJSON["focusType"]:
776                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
777
778            if "assetType" in iJSON.keys() and iJSON["assetType"]:
779                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
780
781            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
782                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
783
784            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
785                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
786
787            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
788                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
789
790            if "currency" in iJSON.keys():
791                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
792
793            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
794                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
795
796            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
797                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
798
799            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
800                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
801
802            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
803                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
804
805            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
806                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
807
808            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
809                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
810
811            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
812                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
813
814            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
815                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
816
817            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
818                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
819
820            iExt = None
821            if iJSON["type"] == "Bonds":
822                info.extend([
823                    splitLine,
824                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
825                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
826                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
827                        iJSON["nominal"]["currency"],
828                    )),
829                ])
830
831                if "floatingCouponFlag" in iJSON.keys():
832                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
833
834                if "amortizationFlag" in iJSON.keys():
835                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
836
837                info.append(splitLine)
838
839                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
840                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
841
842                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
843
844                info.extend([
845                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
846                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
847                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
848                ])
849
850                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
851                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
852                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
853                        iJSON["aciValue"]["currency"]
854                    )))
855
856            if "currentPrice" in iJSON.keys():
857                info.append(splitLine)
858
859                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
860                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
861
862                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
863                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
864                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
865                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
866                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
867
868                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
869                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
870
871                info.extend([
872                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
873                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
874                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
875                    )),
876                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
877                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
878                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
879                    )),
880                    "| Changes between last deal price and last close              | {:<54} |\n".format(
881                        "{:.2f}%{}".format(
882                            iJSON["currentPrice"]["changes"],
883                            " ({}{:.2f} {})".format(
884                                "+" if bondChangesDelta > 0 else "",
885                                bondChangesDelta,
886                                aciCurrency
887                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
888                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
889                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
890                                currency
891                            ),
892                        )
893                    ),
894                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
895                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
897                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
898                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
899                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
900                    )),
901                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
902                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
903                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
904                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
905                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
906                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
907                    )),
908                ])
909
910            if "lot" in iJSON.keys():
911                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
912
913            if "step" in iJSON.keys() and iJSON["step"] != 0:
914                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
915
916            # Add bond payment calendar:
917            if iJSON["type"] == "Bonds":
918                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
919                info.extend(["\n", strCalendar])
920
921            infoText += "".join(info)
922
923            if show:
924                uLogger.info("{}".format(infoText))
925
926            else:
927                uLogger.debug("{}".format(infoText))
928
929            if self.infoFile is not None:
930                with open(self.infoFile, "w", encoding="UTF-8") as fH:
931                    fH.write(infoText)
932
933                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
934
935        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 937    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 938        """
 939        Search and return raw broker's information about instrument by its ticker.
 940        `ticker` must be defined! If debug=True then print all debug messages.
 941
 942        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 943        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 944        :param debug: if `True` then print all debug console messages.
 945        :return: JSON formatted data with information about instrument.
 946        """
 947        tickerJSON = {}
 948        if debug:
 949            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 950
 951        if not self.ticker:
 952            uLogger.warning("self.ticker variable is not be empty!")
 953
 954        else:
 955            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 956                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 957                raise Exception("Instrument not allowed")
 958
 959            if not self.iList:
 960                self.iList = self.Listing()
 961
 962            if self.ticker in self.iList["Shares"].keys():
 963                tickerJSON = self.iList["Shares"][self.ticker]
 964                if debug:
 965                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 966
 967            elif self.ticker in self.iList["Currencies"].keys():
 968                tickerJSON = self.iList["Currencies"][self.ticker]
 969                if debug:
 970                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 971
 972            elif self.ticker in self.iList["Bonds"].keys():
 973                tickerJSON = self.iList["Bonds"][self.ticker]
 974                if debug:
 975                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 976
 977            elif self.ticker in self.iList["Etfs"].keys():
 978                tickerJSON = self.iList["Etfs"][self.ticker]
 979                if debug:
 980                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 981
 982            elif self.ticker in self.iList["Futures"].keys():
 983                tickerJSON = self.iList["Futures"][self.ticker]
 984                if debug:
 985                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 986
 987        if tickerJSON:
 988            self.figi = tickerJSON["figi"]
 989
 990            if requestPrice:
 991                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 992
 993                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 994                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 995
 996                else:
 997                    tickerJSON["currentPrice"]["changes"] = 0
 998
 999            if show:
1000                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1001
1002        else:
1003            if show:
1004                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1005
1006        return tickerJSON

Search and return raw broker's information about instrument by its ticker. ticker must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1008    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1009        """
1010        Search and return raw broker's information about instrument by its FIGI.
1011        `figi` must be defined! If debug=True then print all debug messages.
1012
1013        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1014        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1015        :param debug: if `True` then print all debug console messages.
1016        :return: JSON formatted data with information about instrument.
1017        """
1018        figiJSON = {}
1019        if debug:
1020            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1021
1022        if not self.figi:
1023            uLogger.warning("self.figi variable is not be empty!")
1024
1025        else:
1026            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1027                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1028                raise Exception("Instrument not allowed")
1029
1030            if not self.iList:
1031                self.iList = self.Listing()
1032
1033            for item in self.iList["Shares"].keys():
1034                if self.figi == self.iList["Shares"][item]["figi"]:
1035                    figiJSON = self.iList["Shares"][item]
1036
1037                    if debug:
1038                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1039
1040                    break
1041
1042            if not figiJSON:
1043                for item in self.iList["Currencies"].keys():
1044                    if self.figi == self.iList["Currencies"][item]["figi"]:
1045                        figiJSON = self.iList["Currencies"][item]
1046
1047                        if debug:
1048                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1049
1050                        break
1051
1052            if not figiJSON:
1053                for item in self.iList["Bonds"].keys():
1054                    if self.figi == self.iList["Bonds"][item]["figi"]:
1055                        figiJSON = self.iList["Bonds"][item]
1056
1057                        if debug:
1058                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1059
1060                        break
1061
1062            if not figiJSON:
1063                for item in self.iList["Etfs"].keys():
1064                    if self.figi == self.iList["Etfs"][item]["figi"]:
1065                        figiJSON = self.iList["Etfs"][item]
1066
1067                        if debug:
1068                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1069
1070                        break
1071
1072            if not figiJSON:
1073                for item in self.iList["Futures"].keys():
1074                    if self.figi == self.iList["Futures"][item]["figi"]:
1075                        figiJSON = self.iList["Futures"][item]
1076
1077                        if debug:
1078                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1079
1080                        break
1081
1082        if figiJSON:
1083            self.figi = figiJSON["figi"]
1084            self.ticker = figiJSON["ticker"]
1085
1086            if requestPrice:
1087                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1088
1089                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1090                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1091
1092                else:
1093                    figiJSON["currentPrice"]["changes"] = 0
1094
1095            if show:
1096                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1097
1098        else:
1099            if show:
1100                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1101
1102        return figiJSON

Search and return raw broker's information about instrument by its FIGI. figi must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1104    def GetCurrentPrices(self, show: bool = True) -> dict:
1105        """
1106        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1107        `{"buy": [{"price": 1243.8, "quantity": 193},
1108                  {"price": 1244.0, "quantity": 168},
1109                  {"price": 1244.8, "quantity": 5},
1110                  {"price": 1245.0, "quantity": 61},
1111                  {"price": 1245.4, "quantity": 60}],
1112          "sell": [{"price": 1243.6, "quantity": 8},
1113                   {"price": 1242.6, "quantity": 10},
1114                   {"price": 1242.4, "quantity": 18},
1115                   {"price": 1242.2, "quantity": 50},
1116                   {"price": 1242.0, "quantity": 113}],
1117          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1118        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1119        - sell: list of dicts with Buyers prices,
1120            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1121            - quantity: volume value by current price in lots,
1122        - limitUp: current trade session limit price, maximum,
1123        - limitDown: current trade session limit price, minimum,
1124        - lastPrice: last deal price of the instrument,
1125        - closePrice: previous trade session close price of the instrument.
1126
1127        See also: `SearchByTicker()` and `SearchByFIGI()`.
1128        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1129        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1130
1131        :param show: if `True` then print DOM to log and console.
1132        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1133                 If an error occurred then returns an empty record:
1134                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1135        """
1136        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1137
1138        if self.depth < 1:
1139            uLogger.error("Depth of Market (DOM) must be >=1!")
1140            raise Exception("Incorrect value")
1141
1142        if not (self.ticker or self.figi):
1143            uLogger.error("self.ticker or self.figi variables must be defined!")
1144            raise Exception("Ticker or FIGI required")
1145
1146        if self.ticker and not self.figi:
1147            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1148            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1149
1150        if not self.ticker and self.figi:
1151            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1152            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1153
1154        if not self.figi:
1155            uLogger.error("FIGI is not defined!")
1156            raise Exception("Ticker or FIGI required")
1157
1158        else:
1159            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1160
1161            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1162            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1163            self.body = str({"figi": self.figi, "depth": self.depth})
1164            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1165
1166            if pricesResponse:
1167                # list of dicts with sellers orders:
1168                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1169
1170                # list of dicts with buyers orders:
1171                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1172
1173                # max price of instrument at this time:
1174                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1175
1176                # min price of instrument at this time:
1177                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1178
1179                # last price of deal with instrument:
1180                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1181
1182                # last close price of instrument:
1183                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1184
1185            else:
1186                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1187                uLogger.debug("Server response: {}".format(pricesResponse))
1188
1189            if show:
1190                if prices["buy"] or prices["sell"]:
1191                    info = [
1192                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1193                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1194                            self.ticker,
1195                            self.figi,
1196                            self.depth,
1197                        ),
1198                        "-" * 60, "\n",
1199                        "             Orders of Buyers | Orders of Sellers\n",
1200                        "-" * 60, "\n",
1201                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1202                        "-" * 60, "\n",
1203                    ]
1204
1205                    if not prices["buy"]:
1206                        info.append("                              | No orders!\n")
1207                        sumBuy = 0
1208
1209                    else:
1210                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1211                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1212                        for item in maxMinSorted:
1213                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1214
1215                    if not prices["sell"]:
1216                        info.append("No orders!                    |\n")
1217                        sumSell = 0
1218
1219                    else:
1220                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1221                        for item in prices["sell"]:
1222                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1223
1224                    info.extend([
1225                        "-" * 60, "\n",
1226                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1227                        "-" * 60, "\n",
1228                    ])
1229
1230                    infoText = "".join(info)
1231
1232                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1233
1234                else:
1235                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1236
1237        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1239    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1240        """
1241        This method get and show information about all available broker instruments for current user account.
1242        If `instrumentsFile` string is not empty then also save information to this file.
1243
1244        :param show: if `True` then print results to console, if `False` - print only to file.
1245        :return: multi-lines string with all available broker instruments
1246        """
1247        if not self.iList:
1248            self.iList = self.Listing()
1249
1250        info = [
1251            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1252            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1253        ]
1254
1255        # add instruments count by type:
1256        for iType in self.iList.keys():
1257            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1258
1259        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1260        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1261
1262        # generating info tables with all instruments by type:
1263        for iType in self.iList.keys():
1264            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1265
1266            for instrument in self.iList[iType].keys():
1267                iName = self.iList[iType][instrument]["name"]  # instrument's name
1268                if len(iName) > 57:
1269                    iName = "{}...".format(iName[:54])  # right trim for a long string
1270
1271                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1272                    self.iList[iType][instrument]["ticker"],
1273                    iName,
1274                    self.iList[iType][instrument]["figi"],
1275                    self.iList[iType][instrument]["currency"],
1276                    self.iList[iType][instrument]["lot"],
1277                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1278                ))
1279
1280        infoText = "".join(info)
1281
1282        if show:
1283            uLogger.info(infoText)
1284
1285        if self.instrumentsFile:
1286            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1287                fH.write(infoText)
1288
1289            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1290
1291        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False - print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1293    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1294        """
1295        This method search and show information about instruments by part of its ticker, FIGI or name.
1296        If `searchResultsFile` string is not empty then also save information to this file.
1297
1298        :param pattern: string with part of ticker, FIGI or instrument's name.
1299        :param show: if `True` then print results to console, if `False` - return list of result only.
1300        :return: list of dictionaries with all found instruments.
1301        """
1302        if not self.iList:
1303            self.iList = self.Listing()
1304
1305        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1306        compiledPattern = re.compile(pattern, re.IGNORECASE)
1307
1308        for iType in self.iList:
1309            for instrument in self.iList[iType].values():
1310                searchResult = compiledPattern.search(" ".join(
1311                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1312                ))
1313
1314                if searchResult:
1315                    searchResults[iType][instrument["ticker"]] = instrument
1316
1317        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1318        info = [
1319            "# Search results\n\n",
1320            "* **Search pattern:** [{}]\n".format(pattern),
1321            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1322            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1323        ]
1324        infoShort = info[:]
1325
1326        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1327        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1328        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1329
1330        if resultsLen == 0:
1331            info.append("\nNo results\n")
1332            infoShort.append("\nNo results\n")
1333            uLogger.warning("No results. Try changing your search pattern.")
1334
1335        else:
1336            for iType in searchResults:
1337                iTypeValuesCount = len(searchResults[iType].values())
1338                if iTypeValuesCount > 0:
1339                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1340                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1341
1342                    for instrument in searchResults[iType].values():
1343                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1344                            instrument["type"],
1345                            instrument["ticker"],
1346                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1347                            instrument["figi"],
1348                        ))
1349
1350                    if iTypeValuesCount <= 5:
1351                        infoShort.extend(info[-iTypeValuesCount:])
1352
1353                    else:
1354                        infoShort.extend(info[-5:])
1355                        infoShort.append(skippedLine)
1356
1357        infoText = "".join(info)
1358        infoTextShort = "".join(infoShort)
1359
1360        if show:
1361            uLogger.info(infoTextShort)
1362            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1363
1364        if self.searchResultsFile:
1365            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1366                fH.write(infoText)
1367
1368            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1369
1370        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False - return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1372    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1373        """
1374        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1375
1376        :param instruments: list of strings with tickers or FIGIs.
1377        :return: list with unique instrument FIGIs only.
1378        """
1379        requestedInstruments = []
1380        for iName in instruments:
1381            if iName not in self.aliases.keys():
1382                if iName not in requestedInstruments:
1383                    requestedInstruments.append(iName)
1384
1385            else:
1386                if iName not in requestedInstruments:
1387                    if self.aliases[iName] not in requestedInstruments:
1388                        requestedInstruments.append(self.aliases[iName])
1389
1390        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1391
1392        onlyUniqueFIGIs = []
1393        for iName in requestedInstruments:
1394            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1395                continue
1396
1397            self.ticker = iName
1398            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1399
1400            if not iData:
1401                self.ticker = ""
1402                self.figi = iName
1403
1404                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1405
1406                if not iData:
1407                    self.figi = ""
1408                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1409
1410            if iData and iData["figi"] not in onlyUniqueFIGIs:
1411                onlyUniqueFIGIs.append(iData["figi"])
1412
1413        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1414
1415        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1417    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1418        """
1419        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1420        See limits: https://tinkoff.github.io/investAPI/limits/
1421        If `pricesFile` string is not empty then also save information to this file.
1422
1423        :param instruments: list of strings with tickers or FIGIs.
1424        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1425        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1426                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1427        """
1428        if instruments is None or not instruments:
1429            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1430            raise Exception("Ticker or FIGI required")
1431
1432        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1433
1434        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1435
1436        iList = []  # trying to get info and current prices about all unique instruments:
1437        for self.figi in onlyUniqueFIGIs:
1438            iData = self.SearchByFIGI(requestPrice=True)
1439            iList.append(iData)
1440
1441        self.ShowListOfPrices(iList, show)
1442
1443        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! See limits: https://tinkoff.github.io/investAPI/limits/ If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1445    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1446        """
1447        Show table contains current prices of given instruments.
1448
1449        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1450                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1451        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1452        :return: multilines text in Markdown format as a table contains current prices.
1453        """
1454        infoText = ""
1455
1456        if show or self.pricesFile:
1457            info = [
1458                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1459                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1460                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1461            ]
1462
1463            for item in iList:
1464                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1465                    item["ticker"],
1466                    item["figi"],
1467                    item["type"],
1468                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1469                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1470                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1471                    "{} / {}".format(
1472                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1473                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1474                    ),
1475                    "{} / {}".format(
1476                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1477                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1478                    ),
1479                    item["currency"],
1480                ))
1481
1482            infoText = "".join(info)
1483
1484            if show:
1485                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1486
1487            if self.pricesFile:
1488                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1489                    fH.write(infoText)
1490
1491                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1492
1493        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1495    def RequestTradingStatus(self) -> dict:
1496        """
1497        Requesting trading status for the instrument defined by `figi` variable.
1498        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1499        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1500
1501        :return: dictionary with trading status attributes. Response example:
1502                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1503                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1504        """
1505        if self.figi is None or not self.figi:
1506            uLogger.error("Variable `figi` must be defined for using this method!")
1507            raise Exception("FIGI required")
1508
1509        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1510
1511        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1512        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1513        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1514
1515        uLogger.debug("Records about current trading status successfully received")
1516
1517        return tradingStatus

Requesting trading status for the instrument defined by figi variable. REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1519    def RequestPortfolio(self) -> dict:
1520        """
1521        Requesting actual user's portfolio for current `accountId`.
1522        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1523        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1524
1525        :return: dictionary with user's portfolio.
1526        """
1527        if self.accountId is None or not self.accountId:
1528            uLogger.error("Variable `accountId` must be defined for using this method!")
1529            raise Exception("Account ID required")
1530
1531        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1532
1533        self.body = str({"accountId": self.accountId})
1534        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1535        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1536
1537        uLogger.debug("Records about user's portfolio successfully received")
1538
1539        return rawPortfolio

Requesting actual user's portfolio for current accountId. REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1541    def RequestPositions(self) -> dict:
1542        """
1543        Requesting open positions by currencies and instruments for current `accountId`.
1544        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1545        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1546
1547        :return: dictionary with open positions by instruments.
1548        """
1549        if self.accountId is None or not self.accountId:
1550            uLogger.error("Variable `accountId` must be defined for using this method!")
1551            raise Exception("Account ID required")
1552
1553        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1554
1555        self.body = str({"accountId": self.accountId})
1556        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1557        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1558
1559        uLogger.debug("Records about current open positions successfully received")
1560
1561        return rawPositions

Requesting open positions by currencies and instruments for current accountId. REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1563    def RequestPendingOrders(self) -> list:
1564        """
1565        Requesting current actual pending orders for current `accountId`.
1566        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1567        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1568
1569        :return: list of dictionaries with pending orders.
1570        """
1571        if self.accountId is None or not self.accountId:
1572            uLogger.error("Variable `accountId` must be defined for using this method!")
1573            raise Exception("Account ID required")
1574
1575        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1576
1577        self.body = str({"accountId": self.accountId})
1578        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1579        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1580
1581        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1582
1583        return rawOrders

Requesting current actual pending orders for current accountId. REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1585    def RequestStopOrders(self) -> list:
1586        """
1587        Requesting current actual stop orders for current `accountId`.
1588        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1589        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1590
1591        :return: list of dictionaries with stop orders.
1592        """
1593        if self.accountId is None or not self.accountId:
1594            uLogger.error("Variable `accountId` must be defined for using this method!")
1595            raise Exception("Account ID required")
1596
1597        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1598
1599        self.body = str({"accountId": self.accountId})
1600        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1601        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1602
1603        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1604
1605        return rawStopOrders

Requesting current actual stop orders for current accountId. REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1607    def Overview(self, show: bool = False, details: str = "full") -> dict:
1608        """
1609        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1610        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1611        are defined then also save information to file.
1612
1613        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1614        many requests about the state of the portfolio, and then, based on the received data, a large number
1615        of calculation and statistics are collected.
1616
1617        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1618        :param details: how detailed should the information be? You should specify one of strings:
1619                        `full` - shows full available information about portfolio status (by default),
1620                        `positions` - shows only open positions,
1621                        `digest` - show a short digest of the portfolio status,
1622                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1623                        `orders` - shows only sections of open limits and stop orders.
1624        :return: dictionary with client's raw portfolio and some statistics.
1625        """
1626        if self.accountId is None or not self.accountId:
1627            uLogger.error("Variable `accountId` must be defined for using this method!")
1628            raise Exception("Account ID required")
1629
1630        view = {
1631            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1632                "headers": {},  # list of dictionaries, response headers without "positions" section
1633                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1634                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1635                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1636                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1637                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1638                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1639                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1640                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1641                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1642            },
1643            "stat": {  # --- some statistics calculated using "raw" sections:
1644                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1645                "availableRUB": 0.,  # available rubles (without other currencies)
1646                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1647                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1648                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1649                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1650                "sharesCostRUB": 0.,  # costs of all shares in RUB
1651                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1652                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1653                "futuresCostRUB": 0.,  # costs of all futures in RUB
1654                "Currencies": [],  # list of dictionaries of all currencies statistics
1655                "Shares": [],  # list of dictionaries of all shares statistics
1656                "Bonds": [],  # list of dictionaries of all bonds statistics
1657                "Etfs": [],  # list of dictionaries of all etfs statistics
1658                "Futures": [],  # list of dictionaries of all futures statistics
1659                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1660                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1661                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1662                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1663                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1664            },
1665            "analytics": {  # --- some analytics of portfolio:
1666                "distrByAssets": {},  # portfolio distribution by assets
1667                "distrByCompanies": {},  # portfolio distribution by companies
1668                "distrBySectors": {},  # portfolio distribution by sectors
1669                "distrByCurrencies": {},  # portfolio distribution by currencies
1670                "distrByCountries": {},  # portfolio distribution by countries
1671            }
1672        }
1673
1674        details = details.lower()
1675        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1676        if details not in availableDetails:
1677            details = "full"
1678            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1679
1680        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1681
1682        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1683        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1684        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1685        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1686
1687        # save response headers without "positions" section:
1688        for key in portfolioResponse.keys():
1689            if key != "positions":
1690                view["raw"]["headers"][key] = portfolioResponse[key]
1691
1692            else:
1693                continue
1694
1695        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1696        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1697        for item in portfolioResponse["positions"]:
1698            if item["instrumentType"] == "currency":
1699                self.figi = item["figi"]
1700                curr = self.SearchByFIGI(requestPrice=False)
1701
1702                # current price of currency in RUB:
1703                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1704                    "name": curr["name"],
1705                    "currentPrice": NanoToFloat(
1706                        item["currentPrice"]["units"],
1707                        item["currentPrice"]["nano"]
1708                    ),
1709                }
1710
1711                view["raw"]["Currencies"].append(item)
1712
1713            elif item["instrumentType"] == "share":
1714                view["raw"]["Shares"].append(item)
1715
1716            elif item["instrumentType"] == "bond":
1717                view["raw"]["Bonds"].append(item)
1718
1719            elif item["instrumentType"] == "etf":
1720                view["raw"]["Etfs"].append(item)
1721
1722            elif item["instrumentType"] == "futures":
1723                view["raw"]["Futures"].append(item)
1724
1725            else:
1726                continue
1727
1728        # how many volume of currencies (by ISO currency name) are blocked:
1729        for item in view["raw"]["positions"]["blocked"]:
1730            blocked = NanoToFloat(item["units"], item["nano"])
1731            if blocked > 0:
1732                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1733
1734        # how many volume of instruments (by FIGI) are blocked:
1735        for item in view["raw"]["positions"]["securities"]:
1736            blocked = int(item["blocked"])
1737            if blocked > 0:
1738                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1739
1740        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1741
1742        if "rub" in allBlocked.keys():
1743            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1744
1745        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1746        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1747        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1748        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1749        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1750        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1751        view["stat"]["portfolioCostRUB"] = sum([
1752            view["stat"]["allCurrenciesCostRUB"],
1753            view["stat"]["sharesCostRUB"],
1754            view["stat"]["bondsCostRUB"],
1755            view["stat"]["etfsCostRUB"],
1756            view["stat"]["futuresCostRUB"],
1757        ])
1758
1759        # --- calculating some portfolio statistics:
1760        byComp = {}  # distribution by companies
1761        bySect = {}  # distribution by sectors
1762        byCurr = {}  # distribution by currencies (include RUB)
1763        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1764        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1765
1766        for item in portfolioResponse["positions"]:
1767            self.figi = item["figi"]
1768            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1769
1770            if instrument:
1771                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1772                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1773
1774                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1775                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1776
1777                else:
1778                    blocked = 0
1779
1780                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1781                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1782                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1783                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1784                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1785                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1786                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1787                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1788                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1789                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1790                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1791                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1792
1793                statData = {
1794                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1795                    "ticker": instrument["ticker"],  # ticker by FIGI
1796                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1797                    "volume": volume,  # available volume of instrument
1798                    "lots": lots,  # volume in lots of instrument
1799                    "direction": direction,  # direction of an instrument's position: short or long
1800                    "blocked": blocked,  # blocked volume of currency or instrument
1801                    "currentPrice": curPrice,  # current instrument's price in basic asset
1802                    "average": average,  # current average position price
1803                    "cost": cost,  # current cost of all volume of instrument in basic asset
1804                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1805                    "costRUB": costRUB,  # cost of instrument in ruble
1806                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1807                    "profit": profit,  # expected profit at current moment
1808                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1809                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1810                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1811                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1812                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1813                    "step": instrument["step"],  # minimum price increment
1814                }
1815
1816                # adding distribution by unique countries:
1817                if statData["country"] not in byCountry.keys():
1818                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1819
1820                else:
1821                    byCountry[statData["country"]]["cost"] += costRUB
1822                    byCountry[statData["country"]]["percent"] += percentCostRUB
1823
1824                if item["instrumentType"] != "currency":
1825                    # adding distribution by unique companies:
1826                    if statData["name"]:
1827                        if statData["name"] not in byComp.keys():
1828                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1829
1830                        else:
1831                            byComp[statData["name"]]["cost"] += costRUB
1832                            byComp[statData["name"]]["percent"] += percentCostRUB
1833
1834                    # adding distribution by unique sectors:
1835                    if statData["sector"] not in bySect.keys():
1836                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1837
1838                    else:
1839                        bySect[statData["sector"]]["cost"] += costRUB
1840                        bySect[statData["sector"]]["percent"] += percentCostRUB
1841
1842                # adding distribution by unique currencies:
1843                if currency not in byCurr.keys():
1844                    byCurr[currency] = {
1845                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1846                        "cost": costRUB,
1847                        "percent": percentCostRUB
1848                    }
1849
1850                else:
1851                    byCurr[currency]["cost"] += costRUB
1852                    byCurr[currency]["percent"] += percentCostRUB
1853
1854                # saving statistics for every instrument:
1855                if item["instrumentType"] == "currency":
1856                    view["stat"]["Currencies"].append(statData)
1857
1858                    # update dict with free funds for trading (total - blocked) by currencies
1859                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1860                    view["stat"]["funds"][currency] = {
1861                        "total": volume,
1862                        "totalCostRUB": costRUB,  # total volume cost in rubles
1863                        "free": volume - blocked,
1864                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1865                    }
1866
1867                elif item["instrumentType"] == "share":
1868                    view["stat"]["Shares"].append(statData)
1869
1870                elif item["instrumentType"] == "bond":
1871                    view["stat"]["Bonds"].append(statData)
1872
1873                elif item["instrumentType"] == "etf":
1874                    view["stat"]["Etfs"].append(statData)
1875
1876                elif item["instrumentType"] == "Futures":
1877                    view["stat"]["Futures"].append(statData)
1878
1879                else:
1880                    continue
1881
1882        # total changes in Russian Ruble:
1883        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1884        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1885        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1886        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1887        view["stat"]["funds"]["rub"] = {
1888            "total": view["stat"]["availableRUB"],
1889            "totalCostRUB": view["stat"]["availableRUB"],
1890            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1891            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1892        }
1893
1894        # --- pending orders sector data:
1895        uniquePendingOrders = []
1896        uniquePendingOrdersFIGIs = []
1897        for item in view["raw"]["orders"]:
1898            if item["figi"] not in uniquePendingOrdersFIGIs:
1899                uniquePendingOrdersFIGIs.append(item["figi"])
1900                uniquePendingOrders.append(item)
1901
1902        for item in uniquePendingOrders:
1903            self.figi = item["figi"]
1904            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1905
1906            if instrument:
1907                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1908                orderType = TKS_ORDER_TYPES[item["orderType"]]
1909                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1910                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1911
1912                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1913                if item["direction"] == "ORDER_DIRECTION_BUY":
1914                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1915
1916                else:
1917                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1918
1919                # requested price for order execution:
1920                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1921
1922                # necessary changes in percent to reach target from current price:
1923                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1924
1925                view["stat"]["orders"].append({
1926                    "orderID": item["orderId"],  # orderId number parameter of current order
1927                    "figi": item["figi"],  # FIGI identification
1928                    "ticker": instrument["ticker"],  # ticker name by FIGI
1929                    "lotsRequested": item["lotsRequested"],  # requested lots value
1930                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1931                    "currentPrice": lastPrice,  # current instrument's price for defined action
1932                    "targetPrice": target,  # requested price for order execution in base currency
1933                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1934                    "percentChanges": changes,  # changes in percent to target from current price
1935                    "currency": item["currency"],  # instrument's currency name
1936                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1937                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1938                    "status": orderState,  # order status from TKS_ORDER_STATES
1939                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1940                })
1941
1942        # --- stop orders sector data:
1943        uniqueStopOrders = []
1944        uniqueStopOrdersFIGIs = []
1945        for item in view["raw"]["stopOrders"]:
1946            if item["figi"] not in uniqueStopOrdersFIGIs:
1947                uniqueStopOrdersFIGIs.append(item["figi"])
1948                uniqueStopOrders.append(item)
1949
1950        for item in uniqueStopOrders:
1951            self.figi = item["figi"]
1952            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1953
1954            if instrument:
1955                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1956                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1957                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1958
1959                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1960                if "expirationTime" in item.keys():
1961                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1962                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1963
1964                else:
1965                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1966                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1967
1968                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1969                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1970                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1971
1972                else:
1973                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1974
1975                # requested price when stop-order executed:
1976                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1977
1978                # price for limit-order, set up when stop-order executed:
1979                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1980
1981                # necessary changes in percent to reach target from current price:
1982                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1983
1984                view["stat"]["stopOrders"].append({
1985                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1986                    "figi": item["figi"],  # FIGI identification
1987                    "ticker": instrument["ticker"],  # ticker name by FIGI
1988                    "lotsRequested": item["lotsRequested"],  # requested lots value
1989                    "currentPrice": lastPrice,  # current instrument's price for defined action
1990                    "targetPrice": target,  # requested price for stop-order execution in base currency
1991                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1992                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1993                    "percentChanges": changes,  # changes in percent to target from current price
1994                    "currency": item["currency"],  # instrument's currency name
1995                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1996                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1997                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1998                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1999                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2000                })
2001
2002        # --- calculating data for analytics section:
2003        # portfolio distribution by assets:
2004        view["analytics"]["distrByAssets"] = {
2005            "Ruble": {
2006                "uniques": 1,
2007                "cost": view["stat"]["availableRUB"],
2008                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2009            },
2010            "Currencies": {
2011                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2012                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2013                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2014            },
2015            "Shares": {
2016                "uniques": len(view["stat"]["Shares"]),
2017                "cost": view["stat"]["sharesCostRUB"],
2018                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2019            },
2020            "Bonds": {
2021                "uniques": len(view["stat"]["Bonds"]),
2022                "cost": view["stat"]["bondsCostRUB"],
2023                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2024            },
2025            "Etfs": {
2026                "uniques": len(view["stat"]["Etfs"]),
2027                "cost": view["stat"]["etfsCostRUB"],
2028                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2029            },
2030            "Futures": {
2031                "uniques": len(view["stat"]["Futures"]),
2032                "cost": view["stat"]["futuresCostRUB"],
2033                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2034            },
2035        }
2036
2037        # portfolio distribution by companies:
2038        view["analytics"]["distrByCompanies"]["All money cash"] = {
2039            "ticker": "",
2040            "cost": view["stat"]["allCurrenciesCostRUB"],
2041            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2042        }
2043        view["analytics"]["distrByCompanies"].update(byComp)
2044
2045        # portfolio distribution by sectors:
2046        view["analytics"]["distrBySectors"]["All money cash"] = {
2047            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2048            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2049        }
2050        view["analytics"]["distrBySectors"].update(bySect)
2051
2052        # portfolio distribution by currencies:
2053        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2054            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2055            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2056
2057        view["analytics"]["distrByCurrencies"].update(byCurr)
2058        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2059        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2060
2061        # portfolio distribution by countries:
2062        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2063            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2064            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2065
2066        view["analytics"]["distrByCountries"].update(byCountry)
2067        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2068        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2069
2070        # --- Prepare text statistics overview in human-readable:
2071        if show:
2072            # Whatever the value `details`, header not changes:
2073            info = [
2074                "# Client's portfolio\n\n",
2075                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2076                "* **Account ID:** [{}]\n".format(self.accountId),
2077            ]
2078
2079            if details in ["full", "positions", "digest"]:
2080                info.extend([
2081                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2082                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2083                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2084                        view["stat"]["totalChangesRUB"],
2085                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2086                        view["stat"]["totalChangesPercentRUB"],
2087                    ),
2088                ])
2089
2090            if details in ["full", "positions"]:
2091                info.extend([
2092                    "## Open positions\n\n",
2093                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2094                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2095                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2096                        "{:.2f} ({:.2f}) rub".format(
2097                            view["stat"]["availableRUB"],
2098                            view["stat"]["blockedRUB"],
2099                        )
2100                    )
2101                ])
2102
2103                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2104                    return [
2105                        "|                             |                                 |          |              |              |                     |                              |\n",
2106                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2107                            noTradeStr if noTradeStr else typeStr,
2108                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2109                        ),
2110                    ]
2111
2112                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2113                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2114                        "{} [{}]".format(data["ticker"], data["figi"]),
2115                        "{:.2f} ({:.2f}) {}".format(
2116                            data["volume"],
2117                            data["blocked"],
2118                            data["currency"],
2119                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2120                            data["volume"],
2121                            data["blocked"],
2122                        ),
2123                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2124                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2125                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2126                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2127                        "{}{:.2f} {} ({}{:.2f}%)".format(
2128                            "+" if data["profit"] > 0 else "",
2129                            data["profit"], data["baseCurrencyName"],
2130                            "+" if data["percentProfit"] > 0 else "",
2131                            data["percentProfit"],
2132                        ),
2133                    )
2134
2135                # --- Show currencies section:
2136                if view["stat"]["Currencies"]:
2137                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2138                    for item in view["stat"]["Currencies"]:
2139                        info.append(_InfoStr(item, showCurrencyName=True))
2140
2141                else:
2142                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2143
2144                # --- Show shares section:
2145                if view["stat"]["Shares"]:
2146                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2147
2148                    for item in view["stat"]["Shares"]:
2149                        info.append(_InfoStr(item))
2150
2151                else:
2152                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2153
2154                # --- Show bonds section:
2155                if view["stat"]["Bonds"]:
2156                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2157
2158                    for item in view["stat"]["Bonds"]:
2159                        info.append(_InfoStr(item))
2160
2161                else:
2162                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2163
2164                # --- Show etfs section:
2165                if view["stat"]["Etfs"]:
2166                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2167
2168                    for item in view["stat"]["Etfs"]:
2169                        info.append(_InfoStr(item))
2170
2171                else:
2172                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2173
2174                # --- Show futures section:
2175                if view["stat"]["Futures"]:
2176                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2177
2178                    for item in view["stat"]["Futures"]:
2179                        info.append(_InfoStr(item))
2180
2181                else:
2182                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2183
2184            if details in ["full", "orders"]:
2185                # --- Show pending orders section:
2186                if view["stat"]["orders"]:
2187                    info.extend([
2188                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2189                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2190                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2191                    ])
2192
2193                    for item in view["stat"]["orders"]:
2194                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2195                            "{} [{}]".format(item["ticker"], item["figi"]),
2196                            item["orderID"],
2197                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2198                            "{} {} ({}{:.2f}%)".format(
2199                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2200                                item["baseCurrencyName"],
2201                                "+" if item["percentChanges"] > 0 else "",
2202                                float(item["percentChanges"]),
2203                            ),
2204                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2205                            item["action"],
2206                            item["type"],
2207                            item["date"],
2208                        ))
2209
2210                else:
2211                    info.append("\n## Total pending limit-orders: 0\n")
2212
2213                # --- Show stop orders section:
2214                if view["stat"]["stopOrders"]:
2215                    info.extend([
2216                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2217                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2218                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2219                    ])
2220
2221                    for item in view["stat"]["stopOrders"]:
2222                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2223                            "{} [{}]".format(item["ticker"], item["figi"]),
2224                            item["orderID"],
2225                            item["lotsRequested"],
2226                            "{} {} ({}{:.2f}%)".format(
2227                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2228                                item["baseCurrencyName"],
2229                                "+" if item["percentChanges"] > 0 else "",
2230                                float(item["percentChanges"]),
2231                            ),
2232                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2233                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2234                            item["action"],
2235                            item["type"],
2236                            item["expType"],
2237                            item["createDate"],
2238                            item["expDate"],
2239                        ))
2240
2241                else:
2242                    info.append("\n## Total stop-orders: 0\n")
2243
2244            if details in ["full", "analytics"]:
2245                # -- Show analytics section:
2246                if view["stat"]["portfolioCostRUB"] > 0:
2247                    info.extend([
2248                        "\n# Analytics\n"
2249                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2250                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2251                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2252                            view["stat"]["totalChangesRUB"],
2253                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2254                            view["stat"]["totalChangesPercentRUB"],
2255                        ),
2256                        "\n## Portfolio distribution by assets\n"
2257                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2258                        "|------------|---------|---------|--------------------|\n",
2259                    ])
2260
2261                    for key in view["analytics"]["distrByAssets"].keys():
2262                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2263                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2264                                key,
2265                                view["analytics"]["distrByAssets"][key]["uniques"],
2266                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2267                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2268                            ))
2269
2270                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2271                    info.extend([
2272                        "\n## Portfolio distribution by companies\n"
2273                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2274                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2275                    ])
2276
2277                    for company in view["analytics"]["distrByCompanies"].keys():
2278                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2279                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2280                            info.append("| {} | {:<7} | {:<18} |\n".format(
2281                                "{}{}{}".format(
2282                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2283                                    company,
2284                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2285                                ),
2286                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2287                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2288                            ))
2289
2290                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2291                    info.extend([
2292                        "\n## Portfolio distribution by sectors\n"
2293                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2294                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2295                    ])
2296
2297                    for sector in view["analytics"]["distrBySectors"].keys():
2298                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2299                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2300                                sector,
2301                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2302                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2303                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2304                            ))
2305
2306                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2307                    info.extend([
2308                        "\n## Portfolio distribution by currencies\n"
2309                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2310                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2311                    ])
2312
2313                    for curr in view["analytics"]["distrByCurrencies"].keys():
2314                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2315                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2316                            info.append("| {} | {:<7} | {:<18} |\n".format(
2317                                "[{}] {}{}".format(
2318                                    curr,
2319                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2320                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2321                                ),
2322                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2323                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2324                            ))
2325
2326                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2327                    info.extend([
2328                        "\n## Portfolio distribution by countries\n"
2329                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2330                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2331                    ])
2332
2333                    for country in view["analytics"]["distrByCountries"].keys():
2334                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2335                            nameLen = len(country)
2336                            info.append("| {} | {:<7} | {:<18} |\n".format(
2337                                "{}{}".format(
2338                                    country,
2339                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2340                                ),
2341                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2342                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2343                            ))
2344
2345            infoText = "".join(info)
2346
2347            uLogger.info(infoText)
2348
2349            if details == "full" and self.overviewFile:
2350                filename = self.overviewFile
2351
2352            elif details == "digest" and self.overviewDigestFile:
2353                filename = self.overviewDigestFile
2354
2355            elif details == "positions" and self.overviewPositionsFile:
2356                filename = self.overviewPositionsFile
2357
2358            elif details == "orders" and self.overviewOrdersFile:
2359                filename = self.overviewOrdersFile
2360
2361            elif details == "analytics" and self.overviewAnalyticsFile:
2362                filename = self.overviewAnalyticsFile
2363
2364            else:
2365                filename = ""
2366
2367            if filename:
2368                with open(filename, "w", encoding="UTF-8") as fH:
2369                    fH.write(infoText)
2370
2371                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2372
2373        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be? You should specify one of strings: full - shows full available information about portfolio status (by default), positions - shows only open positions, digest - show a short digest of the portfolio status, analytics - shows only the analytics section and the distribution of the portfolio by various categories, orders - shows only sections of open limits and stop orders.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2375    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2376        """
2377        Returns history operations between two given dates for current `accountId`.
2378        If `reportFile` string is not empty then also save human-readable report.
2379        Shows some statistical data of closed positions.
2380
2381        :param start: see docstring in `GetDatesAsString()` method
2382        :param end: see docstring in `GetDatesAsString()` method
2383        :param show: if `True` then also prints all records to the console.
2384        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2385        :return: original list of dictionaries with history of deals records from API ("operations" key):
2386                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2387                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2388        """
2389        if self.accountId is None or not self.accountId:
2390            uLogger.error("Variable `accountId` must be defined for using this method!")
2391            raise Exception("Account ID required")
2392
2393        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2394
2395        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2396
2397        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2398        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2399        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2400        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2401        customStat = {}  # custom statistics in additional to responseJSON
2402
2403        # --- output report in human-readable format:
2404        if show or self.reportFile:
2405            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2406            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2407            nextDay = ""
2408
2409            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2410
2411            if len(ops) > 0:
2412                customStat = {
2413                    "opsCount": 0,  # total operations count
2414                    "buyCount": 0,  # buy operations
2415                    "sellCount": 0,  # sell operations
2416                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2417                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2418                    "payIn": {"rub": 0.},  # Deposit brokerage account
2419                    "payOut": {"rub": 0.},  # Withdrawals
2420                    "divs": {"rub": 0.},  # Dividends income
2421                    "coupons": {"rub": 0.},  # Coupon's income
2422                    "brokerCom": {"rub": 0.},  # Service commissions
2423                    "serviceCom": {"rub": 0.},  # Service commissions
2424                    "marginCom": {"rub": 0.},  # Margin commissions
2425                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2426                }
2427
2428                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2429                for item in ops:
2430                    if item["state"] == "OPERATION_STATE_EXECUTED":
2431                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2432
2433                        # count buy operations:
2434                        if "_BUY" in item["operationType"]:
2435                            customStat["buyCount"] += 1
2436
2437                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2438                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2439
2440                            else:
2441                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2442
2443                        # count sell operations:
2444                        elif "_SELL" in item["operationType"]:
2445                            customStat["sellCount"] += 1
2446
2447                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2448                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2449
2450                            else:
2451                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2452
2453                        # count incoming operations:
2454                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2455                            if item["payment"]["currency"] in customStat["payIn"].keys():
2456                                customStat["payIn"][item["payment"]["currency"]] += payment
2457
2458                            else:
2459                                customStat["payIn"][item["payment"]["currency"]] = payment
2460
2461                        # count withdrawals operations:
2462                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2463                            if item["payment"]["currency"] in customStat["payOut"].keys():
2464                                customStat["payOut"][item["payment"]["currency"]] += payment
2465
2466                            else:
2467                                customStat["payOut"][item["payment"]["currency"]] = payment
2468
2469                        # count dividends income:
2470                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2471                            if item["payment"]["currency"] in customStat["divs"].keys():
2472                                customStat["divs"][item["payment"]["currency"]] += payment
2473
2474                            else:
2475                                customStat["divs"][item["payment"]["currency"]] = payment
2476
2477                        # count coupon's income:
2478                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2479                            if item["payment"]["currency"] in customStat["coupons"].keys():
2480                                customStat["coupons"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["coupons"][item["payment"]["currency"]] = payment
2484
2485                        # count broker commissions:
2486                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2487                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2488                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2489
2490                            else:
2491                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2492
2493                        # count service commissions:
2494                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2495                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2496                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2497
2498                            else:
2499                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2500
2501                        # count margin commissions:
2502                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2503                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2504                                customStat["marginCom"][item["payment"]["currency"]] += payment
2505
2506                            else:
2507                                customStat["marginCom"][item["payment"]["currency"]] = payment
2508
2509                        # count withholding taxes:
2510                        elif "_TAX" in item["operationType"]:
2511                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2512                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2513
2514                            else:
2515                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2516
2517                        else:
2518                            continue
2519
2520                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2521
2522                # --- view "Actions" lines:
2523                info.extend([
2524                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2525                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2526                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2527                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2528                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2529                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2530                    ),
2531                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2532                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2533                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2534                    ),
2535                ])
2536
2537                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2538                for key in opsKeys:
2539                    if key == "rub":
2540                        continue
2541
2542                    info.extend([
2543                        "|                            |                               | {:<28} |                      |                        |\n".format(
2544                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2545                        ),
2546                        "|                            |                               | {:<28} |                      |                        |\n".format(
2547                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2548                        ),
2549                    ])
2550
2551                info.append(splitLine1)
2552
2553                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2554                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2555                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2556                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2557                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2558                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2559                    )
2560
2561                # --- view "Payments" lines:
2562                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2563                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2564
2565                for key in paymentsKeys:
2566                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2567
2568                info.append(splitLine1)
2569
2570                # --- view "Commissions and taxes" lines:
2571                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2572                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2573
2574                for key in comKeys:
2575                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2576
2577                info.append(splitLine1)
2578
2579                info.extend([
2580                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2581                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2582                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2583                ])
2584
2585            else:
2586                info.append("Broker returned no operations during this period\n")
2587
2588            # --- view "Operations" section:
2589            for item in ops:
2590                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2591                    continue
2592
2593                else:
2594                    self.figi = item["figi"] if item["figi"] else ""
2595                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2596                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2597
2598                    # group of deals during one day:
2599                    if nextDay and item["date"].split("T")[0] != nextDay:
2600                        info.append(splitLine2)
2601                        nextDay = ""
2602
2603                    else:
2604                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2605
2606                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2607                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2608                        self.figi if self.figi else "—",
2609                        instrument["ticker"] if instrument else "—",
2610                        instrument["type"] if instrument else "—",
2611                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2612                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2613                        TKS_OPERATION_STATES[item["state"]],
2614                        TKS_OPERATION_TYPES[item["operationType"]],
2615                    ))
2616
2617            infoText = "".join(info)
2618
2619            if show:
2620                uLogger.info(infoText)
2621
2622            if self.reportFile:
2623                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2624                    fH.write(infoText)
2625
2626                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2627
2628        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2630    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2631        """
2632        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2633
2634        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2635        Warning! Broker server used ISO UTC time by default.
2636
2637        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2638        Also, `historyFile` used to update history with `onlyMissing` parameter.
2639
2640        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2641
2642        :param start: see docstring in `GetDatesAsString()` method.
2643        :param end: see docstring in `GetDatesAsString()` method.
2644        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2645                         `"hour"`, `"day"`. Default: `"hour"`.
2646        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2647                            False by default. Warning! History appends only from last candle to current time
2648                            with always update last candle!
2649        :param csvSep: separator if csv-file is used, `,` by default.
2650        :param show: if `True` then also prints Pandas DataFrame to the console.
2651        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2652                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2653        """
2654        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2655        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2656        history = None  # empty pandas object for history
2657
2658        if interval not in TKS_CANDLE_INTERVALS.keys():
2659            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2660            raise Exception("Incorrect value")
2661
2662        if not (self.ticker or self.figi):
2663            uLogger.error("Ticker or FIGI must be defined!")
2664            raise Exception("Ticker or FIGI required")
2665
2666        if self.ticker and not self.figi:
2667            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2668            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2669
2670        if self.figi and not self.ticker:
2671            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2672            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2673
2674        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2675        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2676        if interval.lower() != "day":
2677            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2678
2679        delta = dtEnd - dtStart  # current UTC time minus last time in file
2680        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2681
2682        # calculate history length in candles:
2683        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2684        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2685            length += 1  # to avoid fraction time
2686
2687        # calculate data blocks count:
2688        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2689
2690        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2691        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2692        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2693        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2694        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2695
2696        tempOld = None  # pandas object for old history, if --only-missing key present
2697        lastTime = None  # datetime object of last old candle in file
2698
2699        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2700            uLogger.debug("--only-missing key present, add only last missing candles...")
2701            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2702
2703            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2704
2705            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2706            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2707            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2708            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2709
2710            # get last datetime object from last string in file or minus 1 delta if file is empty:
2711            if len(tempOld) > 0:
2712                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2713
2714            else:
2715                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2716
2717            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2718
2719        responseJSONs = []  # raw history blocks of data
2720
2721        blockEnd = dtEnd
2722        for item in range(blocks):
2723            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2724            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2725
2726            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2727                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2728            ))
2729
2730            if blockStart == blockEnd:
2731                uLogger.debug("Skipped this zero-length block...")
2732
2733            else:
2734                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2735                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2736                self.body = str({
2737                    "figi": self.figi,
2738                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2739                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2740                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2741                })
2742                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2743
2744                if "code" in responseJSON.keys():
2745                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2746
2747                else:
2748                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2749                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2750
2751                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2752
2753            blockEnd = blockStart
2754
2755        printCount = len(responseJSONs)  # candles to show in console
2756        if responseJSONs:
2757            tempHistory = pd.DataFrame(
2758                data={
2759                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2760                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2761                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2762                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2763                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2764                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2765                    "volume": [int(item["volume"]) for item in responseJSONs],
2766                },
2767                index=range(len(responseJSONs)),
2768                columns=["date", "time", "open", "high", "low", "close", "volume"],
2769            )
2770            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2771            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2772
2773            # append only newest candles to old history if --only-missing key present:
2774            if onlyMissing and tempOld is not None and lastTime is not None:
2775                index = 0  # find start index in tempHistory data:
2776
2777                for i, item in tempHistory.iterrows():
2778                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2779
2780                    if curTime == lastTime:
2781                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2782                        index = i
2783                        printCount = index + 1
2784                        break
2785
2786                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2787
2788            else:
2789                history = tempHistory  # if no `--only-missing` key then load full data from server
2790
2791            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2792
2793        if history is not None and not history.empty:
2794            if show:
2795                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2796                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2797                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2798                ))
2799
2800        else:
2801            uLogger.warning("Received an empty candles history!")
2802
2803        if self.historyFile is not None:
2804            if history is not None and not history.empty:
2805                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2806                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2807
2808            else:
2809                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2810
2811        else:
2812            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2813
2814        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2816    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2817        """
2818        Load candles history from csv-file and return Pandas DataFrame object.
2819
2820        See also: `History()` and `ShowHistoryChart()` methods.
2821
2822        :param filePath: path to csv-file to open.
2823        """
2824        loadedHistory = None  # init candles data object
2825
2826        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2827
2828        if os.path.exists(filePath):
2829            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2830
2831            tfStr = self.priceModel.FormattedDelta(
2832                self.priceModel.timeframe,
2833                "{days} days {hours}h {minutes}m {seconds}s",
2834            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2835                self.priceModel.timeframe,
2836                "{hours}h {minutes}m {seconds}s",
2837            )
2838
2839            if loadedHistory is not None and not loadedHistory.empty:
2840                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2841                    len(loadedHistory),
2842                    tfStr,
2843                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2844                )
2845
2846            else:
2847                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2848
2849        else:
2850            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2851
2852        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2854    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2855        """
2856        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2857
2858        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2859        Default: `index.html` (both for interact and non-interact candlesticks chart).
2860
2861        See also: `History()` and `LoadHistory()` methods.
2862
2863        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2864        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2865                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2866                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2867                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2868        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2869                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2870        """
2871        if isinstance(candles, str):
2872            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2873            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2874
2875        elif isinstance(candles, pd.DataFrame):
2876            self.priceModel.prices = candles  # set candles chain from variable
2877            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2878
2879            if "datetime" not in candles.columns:
2880                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2881
2882        else:
2883            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2884            raise Exception("Incorrect value")
2885
2886        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2887
2888        if interact:
2889            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2890
2891            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2892
2893        else:
2894            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2895
2896            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2897
2898        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2900    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2901        """
2902        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2903        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2904
2905        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2906
2907        :param operation: string "Buy" or "Sell".
2908        :param lots: volume, integer count of lots >= 1.
2909        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2910        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2911        :param expDate: string "Undefined" by default or local date in future,
2912                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2913        :return: JSON with response from broker server.
2914        """
2915        if self.accountId is None or not self.accountId:
2916            uLogger.error("Variable `accountId` must be defined for using this method!")
2917            raise Exception("Account ID required")
2918
2919        if operation is None or not operation or operation not in ("Buy", "Sell"):
2920            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2921            raise Exception("Incorrect value")
2922
2923        if lots is None or lots < 1:
2924            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2925            lots = 1
2926
2927        if tp is None or tp < 0:
2928            tp = 0
2929
2930        if sl is None or sl < 0:
2931            sl = 0
2932
2933        if expDate is None or not expDate:
2934            expDate = "Undefined"
2935
2936        if not (self.ticker or self.figi):
2937            uLogger.error("Ticker or FIGI must be defined!")
2938            raise Exception("Ticker or FIGI required")
2939
2940        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2941        self.ticker = instrument["ticker"]
2942        self.figi = instrument["figi"]
2943
2944        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2945
2946        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2947        self.body = str({
2948            "figi": self.figi,
2949            "quantity": str(lots),
2950            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2951            "accountId": str(self.accountId),
2952            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2953        })
2954        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2955
2956        if "orderId" in response.keys():
2957            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2958                operation, response["orderId"],
2959                self.ticker, self.figi, lots,
2960                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2961                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2962                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2963            ))
2964
2965        else:
2966            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2967
2968        if tp > 0:
2969            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2970
2971        if sl > 0:
2972            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2973
2974        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2976    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2977        """
2978        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2979        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2980
2981        See also: `Order()` and `Trade()` docstrings.
2982
2983        :param lots: volume, integer count of lots >= 1.
2984        :param tp: float > 0, take profit price of stop-order.
2985        :param sl: float > 0, stop loss price of stop-order.
2986        :param expDate: it's a local date in future.
2987                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2988        :return: JSON with response from broker server.
2989        """
2990        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2992    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2993        """
2994        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2995        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2996
2997        See also: `Order()` and `Trade()` docstrings.
2998
2999        :param lots: volume, integer count of lots >= 1.
3000        :param tp: float > 0, take profit price of stop-order.
3001        :param sl: float > 0, stop loss price of stop-order.
3002        :param expDate: it's a local date in the future.
3003                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3004        :return: JSON with response from broker server.
3005        """
3006        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
3008    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
3009        """
3010        Close position of given instruments.
3011
3012        :param tickers: tickers list of instruments that must be closed.
3013        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3014                         This avoids unnecessary downloading data from the server.
3015        """
3016        if not tickers:
3017            uLogger.info("Tickers list is empty, nothing to close.")
3018
3019        else:
3020            if portfolio is None or not portfolio:
3021                portfolio = self.Overview(show=False)
3022
3023            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3024            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
3025
3026            for ticker in tickers:
3027                if ticker not in allOpenedTickers:
3028                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
3029                    continue
3030
3031                # search open trade info about instrument by ticker:
3032                instrument = {}
3033                for iType in TKS_INSTRUMENTS:
3034                    if instrument:
3035                        break
3036
3037                    for item in portfolio["stat"][iType]:
3038                        if item["ticker"] == ticker:
3039                            instrument = item
3040                            break
3041
3042                if instrument:
3043                    self.ticker = ticker
3044                    self.figi = instrument["figi"]
3045
3046                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3047                        self.ticker,
3048                        self.figi,
3049                        int(instrument["volume"]),
3050                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3051                    ))
3052
3053                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3054
3055                    if tradeLots > 0:
3056                        if instrument["blocked"] > 0:
3057                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3058                                instrument["blocked"],
3059                                self.ticker,
3060                                tradeLots,
3061                            ))
3062
3063                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3064                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3065
3066                    else:
3067                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • tickers: tickers list of instruments that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3069    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3070        """
3071        Close all positions of given instruments with defined type.
3072
3073        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3074        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3075                         This avoids unnecessary downloading data from the server.
3076        """
3077        if iType not in TKS_INSTRUMENTS:
3078            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3079
3080        else:
3081            if portfolio is None or not portfolio:
3082                portfolio = self.Overview(show=False)
3083
3084            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3085            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3086
3087            if tickers and portfolio:
3088                self.CloseTrades(tickers, portfolio)
3089
3090            else:
3091                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3093    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3094        """
3095        Universal method to create market or limit orders with all available parameters for current `accountId`.
3096        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3097
3098        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3099        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3100
3101        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3102        then broker immediately open market order as you can do simple --buy or --sell operations!
3103
3104        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3105        When current price will go up or down to target price value then broker opens a limit order.
3106        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3107
3108        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3109
3110        :param operation: string "Buy" or "Sell".
3111        :param orderType: string "Limit" or "Stop".
3112        :param lots: volume, integer count of lots >= 1.
3113        :param targetPrice: target price > 0. This is open trade price for limit order.
3114        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3115                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3116        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3117                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3118                         Stop loss order always executed by market price.
3119        :param expDate: string "Undefined" by default or local date in future.
3120                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3121                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3122                        A limit order has no expiration date, it lasts until the end of the trading day.
3123        :return: JSON with response from broker server.
3124        """
3125        if self.accountId is None or not self.accountId:
3126            uLogger.error("Variable `accountId` must be defined for using this method!")
3127            raise Exception("Account ID required")
3128
3129        if operation is None or not operation or operation not in ("Buy", "Sell"):
3130            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3131            raise Exception("Incorrect value")
3132
3133        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3134            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3135            raise Exception("Incorrect value")
3136
3137        if lots is None or lots < 1:
3138            uLogger.error("You must define trade volume > 0: integer count of lots!")
3139            raise Exception("Incorrect value")
3140
3141        if targetPrice is None or targetPrice <= 0:
3142            uLogger.error("Target price for limit-order must be greater than 0!")
3143            raise Exception("Incorrect value")
3144
3145        if limitPrice is None or limitPrice <= 0:
3146            limitPrice = targetPrice
3147
3148        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3149            stopType = "Limit"
3150
3151        if expDate is None or not expDate:
3152            expDate = "Undefined"
3153
3154        if not (self.ticker or self.figi):
3155            uLogger.error("Tocker or FIGI must be defined!")
3156            raise Exception("Ticker or FIGI required")
3157
3158        response = {}
3159        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3160        self.ticker = instrument["ticker"]
3161        self.figi = instrument["figi"]
3162
3163        if orderType == "Limit":
3164            uLogger.debug(
3165                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3166                    self.ticker, self.figi,
3167                    operation, lots, targetPrice, instrument["currency"],
3168                ))
3169
3170            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3171            self.body = str({
3172                "figi": self.figi,
3173                "quantity": str(lots),
3174                "price": FloatToNano(targetPrice),
3175                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3176                "accountId": str(self.accountId),
3177                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3178            })
3179            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3180
3181            if "orderId" in response.keys():
3182                uLogger.info(
3183                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3184                        response["orderId"],
3185                        self.ticker, self.figi,
3186                        operation, lots, targetPrice, instrument["currency"],
3187                    ))
3188
3189                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3190                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3191                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3192                            targetPrice, instrument["currency"],
3193                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3194                        ))
3195
3196                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3197                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3198                            targetPrice, instrument["currency"],
3199                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3200                        ))
3201
3202            else:
3203                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3204
3205        if orderType == "Stop":
3206            uLogger.debug(
3207                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3208                    self.ticker, self.figi,
3209                    operation, lots,
3210                    targetPrice, instrument["currency"],
3211                    limitPrice, instrument["currency"],
3212                    stopType, expDate,
3213                ))
3214
3215            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3216            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3217            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3218
3219            body = {
3220                "figi": self.figi,
3221                "quantity": str(lots),
3222                "price": FloatToNano(limitPrice),
3223                "stopPrice": FloatToNano(targetPrice),
3224                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3225                "accountId": str(self.accountId),
3226                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3227                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3228            }
3229
3230            if expDateUTC:
3231                body["expireDate"] = expDateUTC
3232
3233            self.body = str(body)
3234            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3235
3236            if "stopOrderId" in response.keys():
3237                uLogger.info(
3238                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3239                        response["stopOrderId"],
3240                        self.ticker, self.figi,
3241                        operation, lots,
3242                        targetPrice, instrument["currency"],
3243                        limitPrice, instrument["currency"],
3244                        TKS_STOP_ORDER_TYPES[stopOrderType],
3245                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3246                    ))
3247
3248                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3249                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3250                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3251                            targetPrice, instrument["currency"],
3252                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3253                        ))
3254
3255                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3256                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3257                            targetPrice, instrument["currency"],
3258                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3259                        ))
3260
3261            else:
3262                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3263
3264        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3266    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3267        """
3268        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3269        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3270        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3271        See also: `Order()` docstring.
3272
3273        :param lots: volume, integer count of lots >= 1.
3274        :param targetPrice: target price > 0. This is open trade price for limit order.
3275        :return: JSON with response from broker server.
3276        """
3277        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3279    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3280        """
3281        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3282        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3283        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3284        target price value then broker opens a limit order. See also: `Order()` docstring.
3285
3286        :param lots: volume, integer count of lots >= 1.
3287        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3288        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3289                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3290        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3291                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3292        :param expDate: string "Undefined" by default or local date in future.
3293                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3294                        This date is converting to UTC format for server.
3295        :return: JSON with response from broker server.
3296        """
3297        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3299    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3300        """
3301        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3302        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3303        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3304        See also: `Order()` docstring.
3305
3306        :param lots: volume, integer count of lots >= 1.
3307        :param targetPrice: target price > 0. This is open trade price for limit order.
3308        :return: JSON with response from broker server.
3309        """
3310        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3312    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3313        """
3314        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3315        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3316        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3317        target price value then broker opens a limit order. See also: `Order()` docstring.
3318
3319        :param lots: volume, integer count of lots >= 1.
3320        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3321        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3322                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3323        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3324                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3325        :param expDate: string "Undefined" by default or local date in future.
3326                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3327                        This date is converting to UTC format for server.
3328        :return: JSON with response from broker server.
3329        """
3330        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3332    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3333        """
3334        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3335
3336        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3337        :param allOrdersIDs: pre-received lists of all active pending orders.
3338                             This avoids unnecessary downloading data from the server.
3339        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3340        """
3341        if self.accountId is None or not self.accountId:
3342            uLogger.error("Variable `accountId` must be defined for using this method!")
3343            raise Exception("Account ID required")
3344
3345        if orderIDs:
3346            if allOrdersIDs is None or not allOrdersIDs:
3347                rawOrders = self.RequestPendingOrders()
3348                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3349
3350            if allStopOrdersIDs is None or not allStopOrdersIDs:
3351                rawStopOrders = self.RequestStopOrders()
3352                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3353
3354            for orderID in orderIDs:
3355                idInPendingOrders = orderID in allOrdersIDs
3356                idInStopOrders = orderID in allStopOrdersIDs
3357
3358                if not (idInPendingOrders or idInStopOrders):
3359                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3360                    continue
3361
3362                else:
3363                    if idInPendingOrders:
3364                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3365
3366                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3367                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3368                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3369                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3370
3371                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3372                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3373                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3374
3375                        else:
3376                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3377
3378                    elif idInStopOrders:
3379                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3380
3381                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3382                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3383                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3384                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3385
3386                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3387                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3388                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3389
3390                        else:
3391                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3392
3393                    else:
3394                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3396    def CloseAllOrders(self) -> None:
3397        """
3398        Gets a list of open pending and stop orders and cancel it all.
3399        """
3400        rawOrders = self.RequestPendingOrders()
3401        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3402        lenOrders = len(allOrdersIDs)
3403
3404        rawStopOrders = self.RequestStopOrders()
3405        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3406        lenSOrders = len(allStopOrdersIDs)
3407
3408        if lenOrders > 0 or lenSOrders > 0:
3409            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3410
3411            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3412
3413        else:
3414            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3416    def CloseAll(self, *args) -> None:
3417        """
3418        Close all available (not blocked) opened trades and orders.
3419
3420        Also, you can select one or more keywords case-insensitive:
3421        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3422
3423        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3424        """
3425        overview = self.Overview(show=False)  # get all open trades info
3426
3427        if len(args) == 0:
3428            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3429            self.CloseAllOrders()  # close all pending and stop orders
3430
3431            for iType in TKS_INSTRUMENTS:
3432                if iType != "Currencies":
3433                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3434
3435        else:
3436            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3437            lowerArgs = [x.lower() for x in args]
3438
3439            if "orders" in lowerArgs:
3440                self.CloseAllOrders()  # close all pending and stop orders
3441
3442            for iType in TKS_INSTRUMENTS:
3443                if iType.lower() in lowerArgs and iType != "Currencies":
3444                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3446    @staticmethod
3447    def ParseOrderParameters(operation, **inputParameters):
3448        """
3449        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3450
3451        :param operation: string "Buy" or "Sell".
3452        :param inputParameters: this is dict of strings that looks like this
3453               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3454               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3455               "prices" key: one or more prices to open limit-orders
3456               Counts of values in lots and prices lists must be equals!
3457        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3458        """
3459        # TODO: update order grid work with api v2
3460        pass
3461        # uLogger.debug("Input parameters: {}".format(inputParameters))
3462        #
3463        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3464        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3465        #     raise Exception("Incorrect value")
3466        #
3467        # if "l" in inputParameters.keys():
3468        #     inputParameters["lots"] = inputParameters.pop("l")
3469        #
3470        # if "p" in inputParameters.keys():
3471        #     inputParameters["prices"] = inputParameters.pop("p")
3472        #
3473        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3474        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3475        #     raise Exception("Incorrect value")
3476        #
3477        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3478        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3479        #
3480        # if len(lots) != len(prices):
3481        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3482        #     raise Exception("Incorrect value")
3483        #
3484        # uLogger.debug("Extracted parameters for orders:")
3485        # uLogger.debug("lots = {}".format(lots))
3486        # uLogger.debug("prices = {}".format(prices))
3487        #
3488        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3489        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3490        # uLogger.debug("Order parameters: {}".format(result))
3491        #
3492        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3494    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3495        """
3496        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3497
3498        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3499        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3500        """
3501        result = False
3502        msg = "Instrument not defined!"
3503
3504        if portfolio is None or not portfolio:
3505            portfolio = self.Overview(show=False)
3506
3507        if self.ticker:
3508            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3509            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3510
3511            for iType in TKS_INSTRUMENTS:
3512                for instrument in portfolio["stat"][iType]:
3513                    if instrument["ticker"] == self.ticker:
3514                        result = True
3515                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3516                        break
3517
3518        elif self.figi:
3519            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3520            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3521
3522            for iType in TKS_INSTRUMENTS:
3523                for instrument in portfolio["stat"][iType]:
3524                    if instrument["figi"] == self.figi:
3525                        result = True
3526                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3527                        break
3528
3529        else:
3530            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3531
3532        uLogger.debug(msg)
3533
3534        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3536    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3537        """
3538        Returns instrument is in the user's portfolio if it presents there.
3539        Instrument must be defined by `ticker` (highly priority) or `figi`.
3540
3541        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3542        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3543        """
3544        result = None
3545        msg = "Instrument not defined!"
3546
3547        if portfolio is None or not portfolio:
3548            portfolio = self.Overview(show=False)
3549
3550        if self.ticker:
3551            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3552            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3553
3554            for iType in TKS_INSTRUMENTS:
3555                for instrument in portfolio["stat"][iType]:
3556                    if instrument["ticker"] == self.ticker:
3557                        result = instrument
3558                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3559                        break
3560
3561        elif self.figi:
3562            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3563            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3564
3565            for iType in TKS_INSTRUMENTS:
3566                for instrument in portfolio["stat"][iType]:
3567                    if instrument["figi"] == self.figi:
3568                        result = instrument
3569                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3570                        break
3571
3572        else:
3573            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3574
3575        uLogger.debug(msg)
3576
3577        return result

Returns instrument is in the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3579    def RequestLimits(self) -> dict:
3580        """
3581        Method for obtaining the available funds for withdrawal for current `accountId`.
3582
3583        See also:
3584        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3585        - `OverviewLimits()` method
3586
3587        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3588                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3589                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3590                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3591        """
3592        if self.accountId is None or not self.accountId:
3593            uLogger.error("Variable `accountId` must be defined for using this method!")
3594            raise Exception("Account ID required")
3595
3596        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3597
3598        self.body = str({"accountId": self.accountId})
3599        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3600        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3601
3602        uLogger.debug("Records about available funds for withdrawal successfully received")
3603
3604        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3606    def OverviewLimits(self, show: bool = False) -> dict:
3607        """
3608        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3609
3610        See also: `RequestLimits()`.
3611
3612        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3613        :return: dict with raw parsed data from server and some calculated statistics about it.
3614        """
3615        if self.accountId is None or not self.accountId:
3616            uLogger.error("Variable `accountId` must be defined for using this method!")
3617            raise Exception("Account ID required")
3618
3619        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3620
3621        view = {
3622            "rawLimits": rawLimits,
3623            "limits": {  # parsed data for every currency:
3624                "money": {  # this is an array of portfolio currency positions
3625                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3626                },
3627                "blocked": {  # this is an array of blocked currency
3628                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3629                },
3630                "blockedGuarantee": {  # this is locked money under collateral for futures
3631                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3632                },
3633            },
3634        }
3635
3636        # --- Prepare text table with limits in human-readable format:
3637        if show:
3638            info = [
3639                "# Withdrawal limits\n\n",
3640                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3641                "* **Account ID:** [{}]\n".format(self.accountId),
3642                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3643                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3644            ]
3645
3646            for curr in view["limits"]["money"].keys():
3647                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3648                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3649                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3650
3651                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3652                    "[{}]".format(curr),
3653                    "{:.2f}".format(view["limits"]["money"][curr]),
3654                    "{:.2f}".format(availableMoney),
3655                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3656                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3657                )
3658
3659                if curr == "rub":
3660                    info.insert(5, infoStr)  # insert at first position in table and after headers
3661
3662                else:
3663                    info.append(infoStr)
3664
3665            infoText = "".join(info)
3666
3667            uLogger.info(infoText)
3668
3669            if self.withdrawalLimitsFile:
3670                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3671                    fH.write(infoText)
3672
3673                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3674
3675        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3677    def RequestAccounts(self) -> dict:
3678        """
3679        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3680
3681        See also:
3682        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3683        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3684        - `OverviewUserInfo()` method
3685
3686        :return: dict with raw data from server that contains accounts info. Example of dict:
3687                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3688                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3689                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3690                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3691        """
3692        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3693
3694        self.body = str({})
3695        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3696        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3697
3698        uLogger.debug("Records about available accounts successfully received")
3699
3700        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3702    def RequestUserInfo(self) -> dict:
3703        """
3704        Method for requesting common user's information.
3705
3706        See also:
3707        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3708        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3709        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3710        - `OverviewUserInfo()` method
3711
3712        :return: dict with raw data from server that contains user's information. Example of dict:
3713                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3714                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3715        """
3716        uLogger.debug("Requesting common user's information. Wait, please...")
3717
3718        self.body = str({})
3719        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3720        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3721
3722        uLogger.debug("Records about current user successfully received")
3723
3724        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3726    def RequestMarginStatus(self, accountId: str = None) -> dict:
3727        """
3728        Method for requesting margin calculation for defined account ID.
3729
3730        See also:
3731        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3732        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3733        - `OverviewUserInfo()` method
3734
3735        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3736        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3737                 Example of responses:
3738                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3739                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3740                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3741                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3742                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3743                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3744        """
3745        if accountId is None or not accountId:
3746            if self.accountId is None or not self.accountId:
3747                uLogger.error("Variable `accountId` must be defined for using this method!")
3748                raise Exception("Account ID required")
3749
3750            else:
3751                accountId = self.accountId  # use `self.accountId` (main ID) by default
3752
3753        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3754
3755        self.body = str({"accountId": accountId})
3756        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3757        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3758
3759        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3760            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3761            rawMargin = {}
3762
3763        else:
3764            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3765
3766        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3768    def RequestTariffLimits(self) -> dict:
3769        """
3770        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3771
3772        See also:
3773        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3774        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3775        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3776        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3777        - `OverviewUserInfo()` method
3778
3779        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3780                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3781                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3782        """
3783        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3784
3785        self.body = str({})
3786        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3787        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3788
3789        uLogger.debug("Records with limits of current tariff successfully received")
3790
3791        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3793    def RequestBondCoupons(self, iJSON: dict) -> dict:
3794        """
3795        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3796        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3797        All dates are in UTC timezone.
3798
3799        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3800        Documentation:
3801        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3802        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3803
3804        See also: `ExtendBondsData()`.
3805
3806        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3807                      If raw iJSON is not data of bond then server returns an error [400] with message:
3808                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3809        :return: dictionary with bond payment calendar. Response example
3810                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3811                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3812                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3813                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3814        """
3815        if iJSON["figi"] is None or not iJSON["figi"]:
3816            uLogger.error("FIGI must be defined for using this method!")
3817            raise Exception("FIGI required")
3818
3819        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3820        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3821
3822        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3823            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3824            self.figi,
3825            startDate,
3826            endDate,
3827        ))
3828
3829        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3830        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3831        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3832
3833        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3834            uLogger.warning("Instrument type is not bond!")
3835
3836        else:
3837            uLogger.debug("Records about bond payment calendar successfully received")
3838
3839        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3841    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3842        """
3843        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3844        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3845        coupon yields, current yields and some statistics etc.
3846
3847        WARNING! This is too long operation if a lot of bonds requested from broker server.
3848
3849        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3850
3851        :param instruments: list of strings with tickers or FIGIs.
3852        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3853                     for further used by data scientists or stock analytics.
3854        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3855                 In XLSX-file and Pandas DataFrame fields mean:
3856                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3857                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3858        """
3859        if instruments is None or not instruments:
3860            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3861            raise Exception("Ticker or FIGI required")
3862
3863        if isinstance(instruments, str):
3864            instruments = [instruments]
3865
3866        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3867
3868        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3869
3870        iCount = len(uniqueInstruments)
3871        tooLong = iCount >= 20
3872        if tooLong:
3873            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3874
3875        bonds = None
3876        for i, self.figi in enumerate(uniqueInstruments):
3877            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3878
3879            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3880                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3881                rawBond = self.SearchByFIGI(requestPrice=True)
3882
3883                # Widen raw data with UTC current time (iData["actualDateTime"]):
3884                actualDate = datetime.now(tzutc())
3885                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3886
3887                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3888                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3889
3890                # Replace some values with human-readable:
3891                iData["nominalCurrency"] = iData["nominal"]["currency"]
3892                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3893                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3894                iData["aciCurrency"] = iData["aciValue"]["currency"]
3895                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3896                iData["issueSize"] = int(iData["issueSize"])
3897                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3898                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3899                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3900                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3901                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3902                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3903                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3904                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3905                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3906                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3907
3908                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3909                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3910                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3911                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3912                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3913                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3914                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3915                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3916                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3917                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3918                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3919
3920                # Widen raw data with calendar data from `rawCalendar` values:
3921                calendarData = []
3922                for item in iData["rawCalendar"]["events"]:
3923                    calendarData.append({
3924                        "couponDate": item["couponDate"],
3925                        "couponNumber": int(item["couponNumber"]),
3926                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3927                        "payCurrency": item["payOneBond"]["currency"],
3928                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3929                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3930                        "couponStartDate": item["couponStartDate"],
3931                        "couponEndDate": item["couponEndDate"],
3932                        "couponPeriod": item["couponPeriod"],
3933                    })
3934
3935                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3936                if "maturityDate" not in iData.keys():
3937                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3938
3939                # Widen raw data with Coupon Rate.
3940                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3941                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3942                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3943                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3944
3945                # Widen raw data with Yield to Maturity (YTM) on current date.
3946                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3947                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3948                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3949                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3950                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3951                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3952
3953                iData["calendar"] = calendarData  # adds calendar at the end
3954
3955                # Remove not used data:
3956                iData.pop("uid")
3957                iData.pop("positionUid")
3958                iData.pop("currentPrice")
3959                iData.pop("rawCalendar")
3960
3961                colNames = list(iData.keys())
3962                if bonds is None:
3963                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3964
3965                else:
3966                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3967
3968            else:
3969                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3970
3971            processed = round(100 * (i + 1) / iCount, 1)
3972            if tooLong and processed % 5 == 0:
3973                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3974
3975            else:
3976                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3977
3978        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3979
3980        # Saving bonds from Pandas DataFrame to XLSX sheet:
3981        if xlsx and self.bondsXLSXFile:
3982            with pd.ExcelWriter(
3983                    path=self.bondsXLSXFile,
3984                    date_format=TKS_DATE_FORMAT,
3985                    datetime_format=TKS_DATE_TIME_FORMAT,
3986                    mode="w",
3987            ) as writer:
3988                bonds.to_excel(
3989                    writer,
3990                    sheet_name="Extended bonds data",
3991                    index=True,
3992                    encoding="UTF-8",
3993                    freeze_panes=(1, 1),
3994                )  # saving as XLSX-file with freeze first row and column as headers
3995
3996            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3997
3998        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4000    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4001        """
4002        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4003
4004        WARNING! This is too long operation if a lot of bonds requested from broker server.
4005
4006        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4007
4008        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4009                        extended information about bonds: main info, current prices, bond payment calendar,
4010                        coupon yields, current yields and some statistics etc.
4011                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4012        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4013                     for further used by data scientists or stock analytics.
4014        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4015        """
4016        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4017            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4018
4019        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4020
4021        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4022        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4023        calendar = None
4024        for bond in extBonds.iterrows():
4025            for item in bond[1]["calendar"]:
4026                cData = {
4027                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4028                    "couponDate": item["couponDate"],
4029                    "figi": bond[1]["figi"],
4030                    "ticker": bond[1]["ticker"],
4031                    "name": bond[1]["name"],
4032                    "couponNumber": item["couponNumber"],
4033                    "payOneBond": item["payOneBond"],
4034                    "payCurrency": item["payCurrency"],
4035                    "couponType": item["couponType"],
4036                    "couponPeriod": item["couponPeriod"],
4037                    "fixDate": item["fixDate"],
4038                    "couponStartDate": item["couponStartDate"],
4039                    "couponEndDate": item["couponEndDate"],
4040                }
4041
4042                if calendar is None:
4043                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4044
4045                else:
4046                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4047
4048        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4049
4050        # Saving calendar from Pandas DataFrame to XLSX sheet:
4051        if xlsx:
4052            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4053
4054            with pd.ExcelWriter(
4055                    path=xlsxCalendarFile,
4056                    date_format=TKS_DATE_FORMAT,
4057                    datetime_format=TKS_DATE_TIME_FORMAT,
4058                    mode="w",
4059            ) as writer:
4060                humanReadable = calendar.copy(deep=True)
4061                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4062                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4063                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4064                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4065                humanReadable.columns = colNames  # human-readable column names
4066
4067                humanReadable.to_excel(
4068                    writer,
4069                    sheet_name="Bond payments calendar",
4070                    index=False,
4071                    encoding="UTF-8",
4072                    freeze_panes=(1, 2),
4073                )  # saving as XLSX-file with freeze first row and column as headers
4074
4075                del humanReadable  # release df in memory
4076
4077            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4078
4079        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4081    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4082        """
4083        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4084        Also, creates Markdown file with calendar data, `calendar.md` by default.
4085
4086        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4087
4088        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4089                        extended information about bonds: main info, current prices, bond payment calendar,
4090                        coupon yields, current yields and some statistics etc.
4091                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4092        :param show: if `True` then also printing bonds payment calendar to the console,
4093                     otherwise save to file `calendarFile` only. `False` by default.
4094        :return: multilines text in Markdown format with bonds payment calendar as a table.
4095        """
4096        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4097            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4098
4099        infoText = "# Bond payments calendar\n\n"
4100
4101        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4102
4103        if not calendar.empty:
4104            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4105
4106            info = [
4107                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4108                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4109            ]
4110
4111            newMonth = False
4112            notOneBond = calendar["figi"].nunique() > 1
4113            for i, bond in enumerate(calendar.iterrows()):
4114                if newMonth and notOneBond:
4115                    info.append(splitLine)
4116
4117                info.append(
4118                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4119                        "  √" if bond[1]["paid"] else "  —",
4120                        bond[1]["couponDate"].split("T")[0],
4121                        bond[1]["figi"],
4122                        bond[1]["ticker"],
4123                        bond[1]["couponNumber"],
4124                        "{} {}".format(
4125                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4126                            bond[1]["payCurrency"],
4127                        ),
4128                        bond[1]["couponType"],
4129                        bond[1]["couponPeriod"],
4130                        bond[1]["fixDate"].split("T")[0],
4131                    )
4132                )
4133
4134                if i < len(calendar.values) - 1:
4135                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4136                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4137                    newMonth = False if curDate.month == nextDate.month else True
4138
4139                else:
4140                    newMonth = False
4141
4142            infoText += "".join(info)
4143
4144            if show:
4145                uLogger.info("{}".format(infoText))
4146
4147            if self.calendarFile is not None:
4148                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4149                    fH.write(infoText)
4150
4151                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4152
4153        else:
4154            infoText += "No data\n"
4155
4156        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4158    def OverviewAccounts(self, show: bool = False) -> dict:
4159        """
4160        Method for parsing and show simple table with all available user accounts.
4161
4162        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4163
4164        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4165        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4166                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4167                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4168                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4169                                                        "closed": "—", "access": "Full access" }, ...}}`
4170        """
4171        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4172
4173        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4174        accounts = {
4175            item["id"]: {
4176                "type": TKS_ACCOUNT_TYPES[item["type"]],
4177                "name": item["name"],
4178                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4179                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4180                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4181                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4182            } for item in rawAccounts["accounts"]
4183        }
4184
4185        # Raw and parsed data with some fields replaced in "stat" section:
4186        view = {
4187            "rawAccounts": rawAccounts,
4188            "stat": accounts,
4189        }
4190
4191        # --- Prepare simple text table with only accounts data in human-readable format:
4192        if show:
4193            info = [
4194                "# User accounts\n\n",
4195                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4196                "| Account ID   | Type                      | Status                    | Name                           |\n",
4197                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4198            ]
4199
4200            for account in view["stat"].keys():
4201                info.extend([
4202                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4203                        account,
4204                        view["stat"][account]["type"],
4205                        view["stat"][account]["status"],
4206                        view["stat"][account]["name"],
4207                    )
4208                ])
4209
4210            infoText = "".join(info)
4211
4212            uLogger.info(infoText)
4213
4214            if self.userAccountsFile:
4215                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4216                    fH.write(infoText)
4217
4218                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4219
4220        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4222    def OverviewUserInfo(self, show: bool = False) -> dict:
4223        """
4224        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4225
4226        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4227
4228        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4229        :return: dict with raw parsed data from server and some calculated statistics about it.
4230        """
4231        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4232        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4233        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4234        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4235        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4236        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4237
4238        # This is dict with parsed common user data:
4239        userInfo = {
4240            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4241            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4242            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4243            "tariff": rawUserInfo["tariff"],
4244        }
4245
4246        # This is an array of dict with parsed margin statuses for every account IDs:
4247        margins = {}
4248        for accountId in accounts.keys():
4249            if rawMargins[accountId]:
4250                margins[accountId] = {
4251                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4252                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4253                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4254                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4255                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4256                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4257                }
4258
4259            else:
4260                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4261
4262        unary = {}  # unary-connection limits
4263        for item in rawTariffLimits["unaryLimits"]:
4264            if item["limitPerMinute"] in unary.keys():
4265                unary[item["limitPerMinute"]].extend(item["methods"])
4266
4267            else:
4268                unary[item["limitPerMinute"]] = item["methods"]
4269
4270        stream = {}  # stream-connection limits
4271        for item in rawTariffLimits["streamLimits"]:
4272            if item["limit"] in stream.keys():
4273                stream[item["limit"]].extend(item["streams"])
4274
4275            else:
4276                stream[item["limit"]] = item["streams"]
4277
4278        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4279        limits = {
4280            "unary": unary,
4281            "stream": stream,
4282        }
4283
4284        # Raw and parsed data as an output result:
4285        view = {
4286            "rawUserInfo": rawUserInfo,
4287            "rawAccounts": rawAccounts,
4288            "rawMargins": rawMargins,
4289            "rawTariffLimits": rawTariffLimits,
4290            "stat": {
4291                "userInfo": userInfo,
4292                "accounts": accounts,
4293                "margins": margins,
4294                "limits": limits,
4295            },
4296        }
4297
4298        # --- Prepare text table with user information in human-readable format:
4299        if show:
4300            info = [
4301                "# Full user information\n\n",
4302                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4303                "## Common information\n\n",
4304                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4305                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4306                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4307                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4308                "\n## User accounts\n\n",
4309            ]
4310
4311            for account in view["stat"]["accounts"].keys():
4312                info.extend([
4313                    "### ID: [{}]\n\n".format(account),
4314                    "| Parameters           | Values                                                       |\n",
4315                    "|----------------------|--------------------------------------------------------------|\n",
4316                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4317                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4318                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4319                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4320                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4321                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4322                ])
4323
4324                if margins[account]:
4325                    info.extend([
4326                        "| Margin status:       | Enabled                                                      |\n",
4327                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4328                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4329                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4330                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4331                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4332                    ])
4333
4334                else:
4335                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4336
4337            info.extend([
4338                "\n## Current user tariff limits\n",
4339                "\nSee also:\n",
4340                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4341                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4342                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4343                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4344                "\n### Unary limits\n",
4345            ])
4346
4347            if unary:
4348                for key, values in sorted(unary.items()):
4349                    info.append("\n* Max requests per minute: {}\n".format(key))
4350
4351                    for value in values:
4352                        info.append("  - {}\n".format(value))
4353
4354            else:
4355                info.append("\nNot available\n")
4356
4357            info.append("\n### Stream limits\n")
4358
4359            if stream:
4360                for key, values in sorted(stream.items()):
4361                    info.append("\n* Max stream connections: {}\n".format(key))
4362
4363                    for value in values:
4364                        info.append("  - {}\n".format(value))
4365
4366            else:
4367                info.append("\nNot available\n")
4368
4369            infoText = "".join(info)
4370
4371            uLogger.info(infoText)
4372
4373            if self.userInfoFile:
4374                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4375                    fH.write(infoText)
4376
4377                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4378
4379        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4382class Args:
4383    """
4384    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4385    """
4386    def __init__(self, **kwargs):
4387        self.__dict__.update(kwargs)
4388
4389    def __getattr__(self, item):
4390        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4386    def __init__(self, **kwargs):
4387        self.__dict__.update(kwargs)
def ParseArgs()
4393def ParseArgs():
4394    """This function get and parse command line keys."""
4395    parser = ArgumentParser()  # command-line string parser
4396
4397    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4398    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4399
4400    # --- options:
4401
4402    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4403    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4404    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4405
4406    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4407    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4408
4409    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4410    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4411
4412    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4413
4414    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4415    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4416    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4417
4418    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4419
4420    # --- commands:
4421
4422    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4423
4424    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4425    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4426    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4427    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4428    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4429    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4430    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4431    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4432
4433    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4434    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4435    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4436    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4437    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4438
4439    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4440    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4441    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4442    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4443
4444    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4445    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4446    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4447
4448    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4449    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4450    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4451    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4452    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4453    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4454    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4455
4456    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4457    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4458    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4459    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4460    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4461
4462    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4463    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4464    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4465
4466    cmdArgs = parser.parse_args()
4467    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4470def Main(**kwargs):
4471    """
4472    Main function for work with TKSBrokerAPI in the console.
4473
4474    See examples:
4475    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4476    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4477    """
4478    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4479
4480    if args.debug_level:
4481        uLogger.level = 10  # always debug level by default
4482        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4483
4484    exitCode = 0
4485    start = datetime.now(tzutc())
4486    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4487        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4488        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4489    ))
4490
4491    # trying to calculate full current version:
4492    buildVersion = __version__
4493    try:
4494        v = version("tksbrokerapi")
4495        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4496
4497    except Exception:
4498        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4499
4500    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4501    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4502
4503    try:
4504        if args.version:
4505            print("TKSBrokerAPI {}".format(buildVersion))
4506            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4507
4508        else:
4509            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4510            server = TinkoffBrokerServer(
4511                token=args.token,
4512                accountId=args.account_id,
4513                useCache=not args.no_cache,
4514            )
4515
4516            # --- set some options:
4517
4518            if args.ticker:
4519                if args.ticker in server.aliasesKeys:
4520                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4521
4522                else:
4523                    server.ticker = args.ticker
4524
4525            if args.figi:
4526                server.figi = args.figi
4527
4528            if args.depth is not None:
4529                server.depth = args.depth
4530
4531            # --- do one of commands:
4532
4533            if args.list:
4534                if args.output is not None:
4535                    server.instrumentsFile = args.output
4536
4537                server.ShowInstrumentsInfo(show=True)
4538
4539            elif args.list_xlsx:
4540                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4541
4542            elif args.bonds_xlsx is not None:
4543                if args.output is not None:
4544                    server.bondsXLSXFile = args.output
4545
4546                if len(args.bonds_xlsx) == 0:
4547                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4548
4549                else:
4550                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4551
4552            elif args.search:
4553                if args.output is not None:
4554                    server.searchResultsFile = args.output
4555
4556                server.SearchInstruments(pattern=args.search[0], show=True)
4557
4558            elif args.info:
4559                if not (args.ticker or args.figi):
4560                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4561                    raise Exception("Ticker or FIGI required")
4562
4563                if args.output is not None:
4564                    server.infoFile = args.output
4565
4566                if args.ticker:
4567                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4568
4569                else:
4570                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4571
4572            elif args.calendar is not None:
4573                if args.output is not None:
4574                    server.calendarFile = args.output
4575
4576                if len(args.calendar) == 0:
4577                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4578
4579                else:
4580                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4581
4582                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4583
4584            elif args.price:
4585                if not (args.ticker or args.figi):
4586                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4587                    raise Exception("Ticker or FIGI required")
4588
4589                server.GetCurrentPrices(show=True)
4590
4591            elif args.prices is not None:
4592                if args.output is not None:
4593                    server.pricesFile = args.output
4594
4595                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4596
4597            elif args.overview:
4598                if args.output is not None:
4599                    server.overviewFile = args.output
4600
4601                server.Overview(show=True, details="full")
4602
4603            elif args.overview_digest:
4604                if args.output is not None:
4605                    server.overviewDigestFile = args.output
4606
4607                server.Overview(show=True, details="digest")
4608
4609            elif args.overview_positions:
4610                if args.output is not None:
4611                    server.overviewPositionsFile = args.output
4612
4613                server.Overview(show=True, details="positions")
4614
4615            elif args.overview_orders:
4616                if args.output is not None:
4617                    server.overviewOrdersFile = args.output
4618
4619                server.Overview(show=True, details="orders")
4620
4621            elif args.overview_analytics:
4622                if args.output is not None:
4623                    server.overviewAnalyticsFile = args.output
4624
4625                server.Overview(show=True, details="analytics")
4626
4627            elif args.deals is not None:
4628                if args.output is not None:
4629                    server.reportFile = args.output
4630
4631                if 0 <= len(args.deals) < 3:
4632                    server.Deals(
4633                        start=args.deals[0] if len(args.deals) >= 1 else None,
4634                        end=args.deals[1] if len(args.deals) == 2 else None,
4635                        show=True,  # Always show deals report in console
4636                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4637                    )
4638
4639                else:
4640                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4641                    raise Exception("Incorrect value")
4642
4643            elif args.history is not None:
4644                if args.output is not None:
4645                    server.historyFile = args.output
4646
4647                if 0 <= len(args.history) < 3:
4648                    dataReceived = server.History(
4649                        start=args.history[0] if len(args.history) >= 1 else None,
4650                        end=args.history[1] if len(args.history) == 2 else None,
4651                        interval="hour" if args.interval is None or not args.interval else args.interval,
4652                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4653                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4654                        show=True,  # shows all downloaded candles in console
4655                    )
4656
4657                    if args.render_chart is not None and dataReceived is not None:
4658                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4659
4660                        server.ShowHistoryChart(
4661                            candles=dataReceived,
4662                            interact=iChart,
4663                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4664                        )
4665
4666                else:
4667                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4668                    raise Exception("Incorrect value")
4669
4670            elif args.load_history is not None:
4671                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4672
4673                if args.render_chart is not None and histData is not None:
4674                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4675                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4676
4677                    server.ShowHistoryChart(
4678                        candles=histData,
4679                        interact=iChart,
4680                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4681                    )
4682
4683            elif args.trade is not None:
4684                if 1 <= len(args.trade) <= 5:
4685                    server.Trade(
4686                        operation=args.trade[0],
4687                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4688                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4689                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4690                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4691                    )
4692
4693                else:
4694                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4695
4696            elif args.buy is not None:
4697                if 0 <= len(args.buy) <= 4:
4698                    server.Buy(
4699                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4700                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4701                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4702                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4703                    )
4704
4705                else:
4706                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4707
4708            elif args.sell is not None:
4709                if 0 <= len(args.sell) <= 4:
4710                    server.Sell(
4711                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4712                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4713                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4714                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4715                    )
4716
4717                else:
4718                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4719
4720            elif args.order:
4721                if 4 <= len(args.order) <= 7:
4722                    server.Order(
4723                        operation=args.order[0],
4724                        orderType=args.order[1],
4725                        lots=int(args.order[2]),
4726                        targetPrice=float(args.order[3]),
4727                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4728                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4729                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4730                    )
4731
4732                else:
4733                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4734
4735            elif args.buy_limit:
4736                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4737
4738            elif args.sell_limit:
4739                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4740
4741            elif args.buy_stop:
4742                if 2 <= len(args.buy_stop) <= 7:
4743                    server.BuyStop(
4744                        lots=int(args.buy_stop[0]),
4745                        targetPrice=float(args.buy_stop[1]),
4746                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4747                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4748                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4749                    )
4750
4751                else:
4752                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4753
4754            elif args.sell_stop:
4755                if 2 <= len(args.sell_stop) <= 7:
4756                    server.SellStop(
4757                        lots=int(args.sell_stop[0]),
4758                        targetPrice=float(args.sell_stop[1]),
4759                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4760                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4761                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4762                    )
4763
4764                else:
4765                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4766
4767            # elif args.buy_order_grid is not None:
4768            #     # update order grid work with api v2
4769            #     if len(args.buy_order_grid) == 2:
4770            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4771            #
4772            #         for order in orderParams:
4773            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4774            #
4775            #     else:
4776            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4777            #
4778            # elif args.sell_order_grid is not None:
4779            #     # update order grid work with api v2
4780            #     if len(args.sell_order_grid) >= 2:
4781            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4782            #
4783            #         for order in orderParams:
4784            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4785            #
4786            #     else:
4787            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4788
4789            elif args.close_order is not None:
4790                server.CloseOrders(args.close_order)  # close only one order
4791
4792            elif args.close_orders is not None:
4793                server.CloseOrders(args.close_orders)  # close list of orders
4794
4795            elif args.close_trade:
4796                if not args.ticker:
4797                    uLogger.error("`--ticker` key is required for this operation!")
4798                    raise Exception("Ticker required")
4799
4800                server.CloseTrades([args.ticker])  # close only one trade
4801
4802            elif args.close_trades is not None:
4803                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4804
4805            elif args.close_all is not None:
4806                server.CloseAll(*args.close_all)
4807
4808            elif args.limits:
4809                if args.output is not None:
4810                    server.withdrawalLimitsFile = args.output
4811
4812                server.OverviewLimits(show=True)
4813
4814            elif args.user_info:
4815                if args.output is not None:
4816                    server.userInfoFile = args.output
4817
4818                server.OverviewUserInfo(show=True)
4819
4820            elif args.account:
4821                if args.output is not None:
4822                    server.userAccountsFile = args.output
4823
4824                server.OverviewAccounts(show=True)
4825
4826            else:
4827                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4828                raise Exception("There is no command to execute")
4829
4830    except Exception:
4831        trace = tb.format_exc()
4832        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4833            if e in trace:
4834                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4835                break
4836
4837        uLogger.debug(trace)
4838        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4839        exitCode = 255  # an error occurred, must be open a ticket for this issue
4840
4841    finally:
4842        finish = datetime.now(tzutc())
4843
4844        if exitCode == 0:
4845            uLogger.debug("All operations were finished success (summary code is 0).")
4846
4847        else:
4848            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4849                os.path.abspath(uLog.defaultLogFile), exitCode,
4850            ))
4851
4852        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4853        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4854            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4855            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4856        ))
4857
4858        if not kwargs:
4859            sys.exit(exitCode)
4860
4861        else:
4862            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: